1 /*
2  * Copyright (C) 2020 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.media.controls.ui.controller
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.view.View
22 import android.view.ViewGroup
23 import androidx.annotation.VisibleForTesting
24 import com.android.systemui.Dumpable
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dump.DumpManager
27 import com.android.systemui.keyguard.MigrateClocksToBlueprint
28 import com.android.systemui.media.controls.ui.view.MediaHost
29 import com.android.systemui.media.controls.ui.view.MediaHostState
30 import com.android.systemui.media.dagger.MediaModule.KEYGUARD
31 import com.android.systemui.plugins.statusbar.StatusBarStateController
32 import com.android.systemui.statusbar.StatusBarState
33 import com.android.systemui.statusbar.SysuiStatusBarStateController
34 import com.android.systemui.statusbar.notification.stack.MediaContainerView
35 import com.android.systemui.statusbar.phone.KeyguardBypassController
36 import com.android.systemui.statusbar.policy.ConfigurationController
37 import com.android.systemui.statusbar.policy.SplitShadeStateController
38 import com.android.systemui.util.asIndenting
39 import com.android.systemui.util.println
40 import com.android.systemui.util.withIncreasedIndent
41 import java.io.PrintWriter
42 import javax.inject.Inject
43 import javax.inject.Named
44 
45 /**
46  * Controls the media notifications on the lock screen, handles its visibility and placement -
47  * switches media player positioning between split pane container vs single pane container
48  */
49 @SysUISingleton
50 class KeyguardMediaController
51 @Inject
52 constructor(
53     @param:Named(KEYGUARD) private val mediaHost: MediaHost,
54     private val bypassController: KeyguardBypassController,
55     private val statusBarStateController: SysuiStatusBarStateController,
56     private val context: Context,
57     configurationController: ConfigurationController,
58     private val splitShadeStateController: SplitShadeStateController,
59     private val logger: KeyguardMediaControllerLogger,
60     dumpManager: DumpManager,
61 ) : Dumpable {
62     private var lastUsedStatusBarState = -1
63 
64     init {
65         dumpManager.registerDumpable(this)
66         statusBarStateController.addCallback(
67             object : StatusBarStateController.StateListener {
onStateChangednull68                 override fun onStateChanged(newState: Int) {
69                     refreshMediaPosition(reason = "StatusBarState.onStateChanged")
70                 }
71 
onDozingChangednull72                 override fun onDozingChanged(isDozing: Boolean) {
73                     refreshMediaPosition(reason = "StatusBarState.onDozingChanged")
74                 }
75             }
76         )
77         configurationController.addCallback(
78             object : ConfigurationController.ConfigurationListener {
onConfigChangednull79                 override fun onConfigChanged(newConfig: Configuration?) {
80                     updateResources()
81                 }
82             }
83         )
84 
85         // First let's set the desired state that we want for this host
86         mediaHost.expansion = MediaHostState.EXPANDED
87         mediaHost.showsOnlyActiveMedia = true
88         mediaHost.falsingProtectionNeeded = true
89 
90         // Let's now initialize this view, which also creates the host view for us.
91         mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
92         updateResources()
93     }
94 
updateResourcesnull95     private fun updateResources() {
96         useSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources)
97     }
98 
99     @VisibleForTesting
100     var useSplitShade = false
101         set(value) {
102             if (field == value) {
103                 return
104             }
105             field = value
106             reattachHostView()
107             refreshMediaPosition(reason = "useSplitShade changed")
108         }
109 
110     /** Is the media player visible? */
111     var visible = false
112         private set
113 
114     var visibilityChangedListener: ((Boolean) -> Unit)? = null
115 
116     /**
117      * Whether the doze wake up animation is delayed and we are currently waiting for it to start.
118      */
119     var isDozeWakeUpAnimationWaiting: Boolean = false
120         set(value) {
121             field = value
122             refreshMediaPosition(reason = "isDozeWakeUpAnimationWaiting changed")
123         }
124 
125     /** single pane media container placed at the top of the notifications list */
126     var singlePaneContainer: MediaContainerView? = null
127         private set
128     private var splitShadeContainer: ViewGroup? = null
129 
130     /**
131      * Attaches media container in single pane mode, situated at the top of the notifications list
132      */
attachSinglePaneContainernull133     fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
134         val needsListener = singlePaneContainer == null
135         singlePaneContainer = mediaView
136         if (needsListener) {
137             // On reinflation we don't want to add another listener
138             mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged)
139         }
140         reattachHostView()
141         onMediaHostVisibilityChanged(mediaHost.visible)
142 
143         singlePaneContainer?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
144     }
145 
146     /** Called whenever the media hosts visibility changes */
onMediaHostVisibilityChangednull147     private fun onMediaHostVisibilityChanged(visible: Boolean) {
148         refreshMediaPosition(reason = "onMediaHostVisibilityChanged")
149 
150         if (visible) {
151             if (MigrateClocksToBlueprint.isEnabled && useSplitShade) {
152                 return
153             }
154             mediaHost.hostView.layoutParams.apply {
155                 height = ViewGroup.LayoutParams.WRAP_CONTENT
156                 width = ViewGroup.LayoutParams.MATCH_PARENT
157             }
158         }
159     }
160 
161     /** Attaches media container in split shade mode, situated to the left of notifications */
attachSplitShadeContainernull162     fun attachSplitShadeContainer(container: ViewGroup) {
163         splitShadeContainer = container
164         reattachHostView()
165         refreshMediaPosition(reason = "attachSplitShadeContainer")
166     }
167 
reattachHostViewnull168     private fun reattachHostView() {
169         val inactiveContainer: ViewGroup?
170         val activeContainer: ViewGroup?
171         if (useSplitShade) {
172             activeContainer = splitShadeContainer
173             inactiveContainer = singlePaneContainer
174         } else {
175             inactiveContainer = splitShadeContainer
176             activeContainer = singlePaneContainer
177         }
178         if (inactiveContainer?.childCount == 1) {
179             inactiveContainer.removeAllViews()
180         }
181         if (activeContainer?.childCount == 0) {
182             // Detach the hostView from its parent view if exists
183             mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
184             activeContainer.addView(mediaHost.hostView)
185         }
186     }
187 
refreshMediaPositionnull188     fun refreshMediaPosition(reason: String) {
189         val currentState = statusBarStateController.state
190 
191         val keyguardOrUserSwitcher = (currentState == StatusBarState.KEYGUARD)
192         // mediaHost.visible required for proper animations handling
193         val isMediaHostVisible = mediaHost.visible
194         val isBypassNotEnabled = !bypassController.bypassEnabled
195         val useSplitShade = useSplitShade
196         val shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade()
197         visible =
198             isMediaHostVisible &&
199                 isBypassNotEnabled &&
200                 keyguardOrUserSwitcher &&
201                 shouldBeVisibleForSplitShade
202         logger.logRefreshMediaPosition(
203             reason = reason,
204             visible = visible,
205             useSplitShade = useSplitShade,
206             currentState = currentState,
207             keyguardOrUserSwitcher = keyguardOrUserSwitcher,
208             mediaHostVisible = isMediaHostVisible,
209             bypassNotEnabled = isBypassNotEnabled,
210             shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade,
211         )
212         val currActiveContainer = activeContainer
213 
214         logger.logActiveMediaContainer("before refreshMediaPosition", currActiveContainer)
215         if (visible) {
216             showMediaPlayer()
217         } else {
218             hideMediaPlayer()
219         }
220         logger.logActiveMediaContainer("after refreshMediaPosition", currActiveContainer)
221 
222         lastUsedStatusBarState = currentState
223     }
224 
shouldBeVisibleForSplitShadenull225     private fun shouldBeVisibleForSplitShade(): Boolean {
226         if (!useSplitShade) {
227             return true
228         }
229         // We have to explicitly hide media for split shade when on AOD, as it is a child view of
230         // keyguard status view, and nothing hides keyguard status view on AOD.
231         // When using the double-line clock, it is not an issue, as media gets implicitly hidden
232         // by the clock. This is not the case for single-line clock though.
233         // For single shade, we don't need to do it, because media is a child of NSSL, which already
234         // gets hidden on AOD.
235         // Media also has to be hidden when waking up from dozing, and the doze wake up animation is
236         // delayed and waiting to be started.
237         // This is to stay in sync with the delaying of the horizontal alignment of the rest of the
238         // keyguard container, that is also delayed until the "wait" is over.
239         // If we show media during this waiting period, the shade will still be centered, and using
240         // the entire width of the screen, and making media show fully stretched.
241         return !statusBarStateController.isDozing && !isDozeWakeUpAnimationWaiting
242     }
243 
showMediaPlayernull244     private fun showMediaPlayer() {
245         if (useSplitShade) {
246             setVisibility(splitShadeContainer, View.VISIBLE)
247             setVisibility(singlePaneContainer, View.GONE)
248         } else {
249             setVisibility(singlePaneContainer, View.VISIBLE)
250             setVisibility(splitShadeContainer, View.GONE)
251         }
252     }
253 
hideMediaPlayernull254     private fun hideMediaPlayer() {
255         // always hide splitShadeContainer as it's initially visible and may influence layout
256         setVisibility(splitShadeContainer, View.GONE)
257         setVisibility(singlePaneContainer, View.GONE)
258     }
259 
setVisibilitynull260     private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
261         val currentMediaContainer = view ?: return
262 
263         val isVisible = newVisibility == View.VISIBLE
264 
265         if (currentMediaContainer is MediaContainerView) {
266             val previousVisibility = currentMediaContainer.visibility
267 
268             currentMediaContainer.setKeyguardVisibility(isVisible)
269             if (previousVisibility != newVisibility) {
270                 visibilityChangedListener?.invoke(isVisible)
271             }
272         } else {
273             currentMediaContainer.visibility = newVisibility
274         }
275     }
276 
dumpnull277     override fun dump(pw: PrintWriter, args: Array<out String>) {
278         pw.asIndenting().run {
279             println("KeyguardMediaController")
280             withIncreasedIndent {
281                 println("Self", this@KeyguardMediaController)
282                 println("visible", visible)
283                 println("useSplitShade", useSplitShade)
284                 println("bypassController.bypassEnabled", bypassController.bypassEnabled)
285                 println("isDozeWakeUpAnimationWaiting", isDozeWakeUpAnimationWaiting)
286                 println("singlePaneContainer", singlePaneContainer)
287                 println("splitShadeContainer", splitShadeContainer)
288                 if (lastUsedStatusBarState != -1) {
289                     println(
290                         "lastUsedStatusBarState",
291                         StatusBarState.toString(lastUsedStatusBarState)
292                     )
293                 }
294                 println(
295                     "statusBarStateController.state",
296                     StatusBarState.toString(statusBarStateController.state)
297                 )
298             }
299         }
300     }
301 
302     // This field is only used to log current active container.
303     private val activeContainer: ViewGroup?
304         get() = if (useSplitShade) splitShadeContainer else singlePaneContainer
305 }
306