1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.phone.panelstate
18 
19 import android.annotation.IntDef
20 import android.util.Log
21 import androidx.annotation.FloatRange
22 import com.android.systemui.dagger.SysUISingleton
23 import javax.inject.Inject
24 
25 /**
26  * A class responsible for managing the notification panel's current state.
27  *
28  * TODO(b/200063118): Make this class the one source of truth for the state of panel expansion.
29  */
30 @SysUISingleton
31 class PanelExpansionStateManager @Inject constructor() {
32 
33     private val expansionListeners = mutableListOf<PanelExpansionListener>()
34     private val stateListeners = mutableListOf<PanelStateListener>()
35 
36     @PanelState private var state: Int = STATE_CLOSED
37     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
38     private var expanded: Boolean = false
39     private var tracking: Boolean = false
40 
41     /**
42      * Adds a listener that will be notified when the panel expansion fraction has changed.
43      *
44      * Listener will also be immediately notified with the current values.
45      */
46     fun addExpansionListener(listener: PanelExpansionListener) {
47         expansionListeners.add(listener)
48         listener.onPanelExpansionChanged(fraction, expanded, tracking)
49     }
50 
51     /** Removes an expansion listener. */
52     fun removeExpansionListener(listener: PanelExpansionListener) {
53         expansionListeners.remove(listener)
54     }
55 
56     /** Adds a listener that will be notified when the panel state has changed. */
57     fun addStateListener(listener: PanelStateListener) {
58         stateListeners.add(listener)
59     }
60 
61     /** Removes a state listener. */
62     fun removeStateListener(listener: PanelStateListener) {
63         stateListeners.remove(listener)
64     }
65 
66     /** Returns true if the panel is currently closed and false otherwise. */
67     fun isClosed(): Boolean = state == STATE_CLOSED
68 
69     /**
70      * Called when the panel expansion has changed.
71      *
72      * @param fraction the fraction from the expansion in [0, 1]
73      * @param expanded whether the panel is currently expanded; this is independent from the
74      * fraction as the panel also might be expanded if the fraction is 0.
75      * @param tracking whether we're currently tracking the user's gesture.
76      */
77     fun onPanelExpansionChanged(
78         @FloatRange(from = 0.0, to = 1.0) fraction: Float,
79         expanded: Boolean,
80         tracking: Boolean
81     ) {
82         require(!fraction.isNaN()) { "fraction cannot be NaN" }
83         val oldState = state
84 
85         this.fraction = fraction
86         this.expanded = expanded
87         this.tracking = tracking
88 
89         var fullyClosed = true
90         var fullyOpened = false
91 
92         if (expanded) {
93             if (this.state == STATE_CLOSED) {
94                 updateStateInternal(STATE_OPENING)
95             }
96             fullyClosed = false
97             fullyOpened = fraction >= 1f
98         }
99 
100         if (fullyOpened && !tracking) {
101             updateStateInternal(STATE_OPEN)
102         } else if (fullyClosed && !tracking && this.state != STATE_CLOSED) {
103             updateStateInternal(STATE_CLOSED)
104         }
105 
106         debugLog(
107             "panelExpansionChanged:" +
108                     "start state=${oldState.stateToString()} " +
109                     "end state=${state.stateToString()} " +
110                     "f=$fraction " +
111                     "expanded=$expanded " +
112                     "tracking=$tracking" +
113                     "${if (fullyOpened) " fullyOpened" else ""} " +
114                     if (fullyClosed) " fullyClosed" else ""
115         )
116 
117         expansionListeners.forEach { it.onPanelExpansionChanged(fraction, expanded, tracking) }
118     }
119 
120     /** Updates the panel state if necessary.  */
121     fun updateState(@PanelState state: Int) {
122         debugLog("update state: ${this.state.stateToString()} -> ${state.stateToString()}")
123         if (this.state != state) {
124             updateStateInternal(state)
125         }
126     }
127 
128     private fun updateStateInternal(@PanelState state: Int) {
129         debugLog("go state: ${this.state.stateToString()} -> ${state.stateToString()}")
130         this.state = state
131         stateListeners.forEach { it.onPanelStateChanged(state) }
132     }
133 
134     private fun debugLog(msg: String) {
135         if (!DEBUG) return
136         Log.v(TAG, msg)
137     }
138 }
139 
140 /** Enum for the current state of the panel.  */
141 @Retention(AnnotationRetention.SOURCE)
142 @IntDef(value = [STATE_CLOSED, STATE_OPENING, STATE_OPEN])
143 internal annotation class PanelState
144 
145 const val STATE_CLOSED = 0
146 const val STATE_OPENING = 1
147 const val STATE_OPEN = 2
148 
149 @PanelState
150 private fun Int.stateToString(): String {
151     return when (this) {
152         STATE_CLOSED -> "CLOSED"
153         STATE_OPENING -> "OPENING"
154         STATE_OPEN -> "OPEN"
155         else -> this.toString()
156     }
157 }
158 
159 private const val DEBUG = false
160 private val TAG = PanelExpansionStateManager::class.simpleName
161