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.shade
18 
19 import android.annotation.IntDef
20 import android.os.Trace
21 import android.os.Trace.TRACE_TAG_APP as TRACE_TAG
22 import android.util.Log
23 import androidx.annotation.FloatRange
24 import com.android.systemui.dagger.SysUISingleton
25 import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener
26 import com.android.systemui.util.Compile
27 import java.util.concurrent.CopyOnWriteArrayList
28 import javax.inject.Inject
29 
30 /**
31  * A class responsible for managing the notification panel's current state.
32  *
33  * TODO(b/200063118): Make this class the one source of truth for the state of panel expansion.
34  */
35 @SysUISingleton
36 class ShadeExpansionStateManager @Inject constructor() : ShadeStateEvents {
37 
38     private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
39     private val fullExpansionListeners = CopyOnWriteArrayList<ShadeFullExpansionListener>()
40     private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>()
41     private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
42     private val shadeStateEventsListeners = CopyOnWriteArrayList<ShadeStateEventsListener>()
43 
44     @PanelState private var state: Int = STATE_CLOSED
45     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
46     private var expanded: Boolean = false
47     private var qsExpanded: Boolean = false
48     private var tracking: Boolean = false
49     private var dragDownPxAmount: Float = 0f
50 
51     /**
52      * Adds a listener that will be notified when the panel expansion fraction has changed and
53      * returns the current state in a ShadeExpansionChangeEvent for legacy purposes (b/23035507).
54      *
55      * @see #addExpansionListener
56      */
57     fun addExpansionListener(listener: ShadeExpansionListener): ShadeExpansionChangeEvent {
58         expansionListeners.add(listener)
59         return ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
60     }
61 
62     /** Removes an expansion listener. */
63     fun removeExpansionListener(listener: ShadeExpansionListener) {
64         expansionListeners.remove(listener)
65     }
66 
67     fun addFullExpansionListener(listener: ShadeFullExpansionListener) {
68         fullExpansionListeners.add(listener)
69         listener.onShadeExpansionFullyChanged(qsExpanded)
70     }
71 
72     fun removeFullExpansionListener(listener: ShadeFullExpansionListener) {
73         fullExpansionListeners.remove(listener)
74     }
75 
76     fun addQsExpansionListener(listener: ShadeQsExpansionListener) {
77         qsExpansionListeners.add(listener)
78         listener.onQsExpansionChanged(qsExpanded)
79     }
80 
81     fun removeQsExpansionListener(listener: ShadeQsExpansionListener) {
82         qsExpansionListeners.remove(listener)
83     }
84 
85     /** Adds a listener that will be notified when the panel state has changed. */
86     fun addStateListener(listener: ShadeStateListener) {
87         stateListeners.add(listener)
88     }
89 
90     /** Removes a state listener. */
91     fun removeStateListener(listener: ShadeStateListener) {
92         stateListeners.remove(listener)
93     }
94 
95     override fun addShadeStateEventsListener(listener: ShadeStateEventsListener) {
96         shadeStateEventsListeners.addIfAbsent(listener)
97     }
98 
99     override fun removeShadeStateEventsListener(listener: ShadeStateEventsListener) {
100         shadeStateEventsListeners.remove(listener)
101     }
102 
103     /** Returns true if the panel is currently closed and false otherwise. */
104     fun isClosed(): Boolean = state == STATE_CLOSED
105 
106     /**
107      * Called when the panel expansion has changed.
108      *
109      * @param fraction the fraction from the expansion in [0, 1]
110      * @param expanded whether the panel is currently expanded; this is independent from the
111      *   fraction as the panel also might be expanded if the fraction is 0.
112      * @param tracking whether we're currently tracking the user's gesture.
113      */
114     fun onPanelExpansionChanged(
115         @FloatRange(from = 0.0, to = 1.0) fraction: Float,
116         expanded: Boolean,
117         tracking: Boolean,
118         dragDownPxAmount: Float
119     ) {
120         require(!fraction.isNaN()) { "fraction cannot be NaN" }
121         val oldState = state
122 
123         this.fraction = fraction
124         this.expanded = expanded
125         this.tracking = tracking
126         this.dragDownPxAmount = dragDownPxAmount
127 
128         var fullyClosed = true
129         var fullyOpened = false
130 
131         if (expanded) {
132             if (this.state == STATE_CLOSED) {
133                 updateStateInternal(STATE_OPENING)
134             }
135             fullyClosed = false
136             fullyOpened = fraction >= 1f
137         }
138 
139         if (fullyOpened && !tracking) {
140             updateStateInternal(STATE_OPEN)
141         } else if (fullyClosed && !tracking && this.state != STATE_CLOSED) {
142             updateStateInternal(STATE_CLOSED)
143         }
144 
145         debugLog(
146             "panelExpansionChanged:" +
147                 "start state=${oldState.panelStateToString()} " +
148                 "end state=${state.panelStateToString()} " +
149                 "f=$fraction " +
150                 "expanded=$expanded " +
151                 "tracking=$tracking " +
152                 "dragDownPxAmount=$dragDownPxAmount " +
153                 "${if (fullyOpened) " fullyOpened" else ""} " +
154                 if (fullyClosed) " fullyClosed" else ""
155         )
156 
157         if (Trace.isTagEnabled(TRACE_TAG)) {
158             Trace.traceCounter(TRACE_TAG, "panel_expansion", (fraction * 100).toInt())
159             if (state != oldState) {
160                 Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK_NAME, 0)
161                 Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK_NAME, state.panelStateToString(), 0)
162             }
163         }
164 
165         val expansionChangeEvent =
166             ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
167         expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) }
168     }
169 
170     /** Called when the quick settings expansion changes to fully expanded or collapsed. */
171     fun onQsExpansionChanged(qsExpanded: Boolean) {
172         this.qsExpanded = qsExpanded
173 
174         debugLog("qsExpanded=$qsExpanded")
175         qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) }
176     }
177 
178     fun onShadeExpansionFullyChanged(isExpanded: Boolean) {
179         this.expanded = isExpanded
180 
181         debugLog("expanded=$isExpanded")
182         fullExpansionListeners.forEach { it.onShadeExpansionFullyChanged(isExpanded) }
183     }
184 
185     /** Updates the panel state if necessary. */
186     fun updateState(@PanelState state: Int) {
187         debugLog(
188             "update state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}"
189         )
190         if (this.state != state) {
191             updateStateInternal(state)
192         }
193     }
194 
195     private fun updateStateInternal(@PanelState state: Int) {
196         debugLog("go state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}")
197         this.state = state
198         stateListeners.forEach { it.onPanelStateChanged(state) }
199     }
200 
201     fun notifyLaunchingActivityChanged(isLaunchingActivity: Boolean) {
202         for (cb in shadeStateEventsListeners) {
203             cb.onLaunchingActivityChanged(isLaunchingActivity)
204         }
205     }
206 
207     fun notifyPanelCollapsingChanged(isCollapsing: Boolean) {
208         for (cb in shadeStateEventsListeners) {
209             cb.onPanelCollapsingChanged(isCollapsing)
210         }
211     }
212 
213     fun notifyExpandImmediateChange(expandImmediateEnabled: Boolean) {
214         for (cb in shadeStateEventsListeners) {
215             cb.onExpandImmediateChanged(expandImmediateEnabled)
216         }
217     }
218 
219     private fun debugLog(msg: String) {
220         if (!DEBUG) return
221         Log.v(TAG, msg)
222     }
223 
224     companion object {
225         private const val TRACK_NAME = "ShadeExpansionState"
226     }
227 }
228 
229 /** Enum for the current state of the panel. */
230 @Retention(AnnotationRetention.SOURCE)
231 @IntDef(value = [STATE_CLOSED, STATE_OPENING, STATE_OPEN])
232 internal annotation class PanelState
233 
234 const val STATE_CLOSED = 0
235 const val STATE_OPENING = 1
236 const val STATE_OPEN = 2
237 
238 @PanelState
239 fun Int.panelStateToString(): String {
240     return when (this) {
241         STATE_CLOSED -> "CLOSED"
242         STATE_OPENING -> "OPENING"
243         STATE_OPEN -> "OPEN"
244         else -> this.toString()
245     }
246 }
247 
248 private val TAG = ShadeExpansionStateManager::class.simpleName
249 private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
250