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 */
addExpansionListenernull57 fun addExpansionListener(listener: ShadeExpansionListener): ShadeExpansionChangeEvent {
58 expansionListeners.add(listener)
59 return ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
60 }
61
62 /** Removes an expansion listener. */
removeExpansionListenernull63 fun removeExpansionListener(listener: ShadeExpansionListener) {
64 expansionListeners.remove(listener)
65 }
66
addFullExpansionListenernull67 fun addFullExpansionListener(listener: ShadeFullExpansionListener) {
68 fullExpansionListeners.add(listener)
69 listener.onShadeExpansionFullyChanged(qsExpanded)
70 }
71
removeFullExpansionListenernull72 fun removeFullExpansionListener(listener: ShadeFullExpansionListener) {
73 fullExpansionListeners.remove(listener)
74 }
75
addQsExpansionListenernull76 fun addQsExpansionListener(listener: ShadeQsExpansionListener) {
77 qsExpansionListeners.add(listener)
78 listener.onQsExpansionChanged(qsExpanded)
79 }
80
removeQsExpansionListenernull81 fun removeQsExpansionListener(listener: ShadeQsExpansionListener) {
82 qsExpansionListeners.remove(listener)
83 }
84
85 /** Adds a listener that will be notified when the panel state has changed. */
addStateListenernull86 fun addStateListener(listener: ShadeStateListener) {
87 stateListeners.add(listener)
88 }
89
90 /** Removes a state listener. */
removeStateListenernull91 fun removeStateListener(listener: ShadeStateListener) {
92 stateListeners.remove(listener)
93 }
94
addShadeStateEventsListenernull95 override fun addShadeStateEventsListener(listener: ShadeStateEventsListener) {
96 shadeStateEventsListeners.addIfAbsent(listener)
97 }
98
removeShadeStateEventsListenernull99 override fun removeShadeStateEventsListener(listener: ShadeStateEventsListener) {
100 shadeStateEventsListeners.remove(listener)
101 }
102
103 /** Returns true if the panel is currently closed and false otherwise. */
isClosednull104 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. */
onQsExpansionChangednull171 fun onQsExpansionChanged(qsExpanded: Boolean) {
172 this.qsExpanded = qsExpanded
173
174 debugLog("qsExpanded=$qsExpanded")
175 qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) }
176 }
177
onShadeExpansionFullyChangednull178 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. */
updateStatenull186 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
updateStateInternalnull195 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
notifyLaunchingActivityChangednull201 fun notifyLaunchingActivityChanged(isLaunchingActivity: Boolean) {
202 for (cb in shadeStateEventsListeners) {
203 cb.onLaunchingActivityChanged(isLaunchingActivity)
204 }
205 }
206
notifyPanelCollapsingChangednull207 fun notifyPanelCollapsingChanged(isCollapsing: Boolean) {
208 for (cb in shadeStateEventsListeners) {
209 cb.onPanelCollapsingChanged(isCollapsing)
210 }
211 }
212
notifyExpandImmediateChangenull213 fun notifyExpandImmediateChange(expandImmediateEnabled: Boolean) {
214 for (cb in shadeStateEventsListeners) {
215 cb.onExpandImmediateChanged(expandImmediateEnabled)
216 }
217 }
218
debugLognull219 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
panelStateToStringnull239 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