• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.graphics.Rect
22 import android.view.View
23 import android.view.ViewGroup
24 import androidx.annotation.VisibleForTesting
25 import com.android.systemui.Dumpable
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.dump.DumpManager
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.shade.ShadeDisplayAware
33 import com.android.systemui.statusbar.StatusBarState
34 import com.android.systemui.statusbar.SysuiStatusBarStateController
35 import com.android.systemui.statusbar.notification.stack.MediaContainerView
36 import com.android.systemui.statusbar.phone.KeyguardBypassController
37 import com.android.systemui.statusbar.policy.ConfigurationController
38 import com.android.systemui.statusbar.policy.SplitShadeStateController
39 import com.android.systemui.util.asIndenting
40 import com.android.systemui.util.println
41 import com.android.systemui.util.withIncreasedIndent
42 import java.io.PrintWriter
43 import javax.inject.Inject
44 import javax.inject.Named
45 
46 /**
47  * Controls the media notifications on the lock screen, handles its visibility and placement -
48  * switches media player positioning between split pane container vs single pane container
49  */
50 @SysUISingleton
51 class KeyguardMediaController
52 @Inject
53 constructor(
54     @param:Named(KEYGUARD) private val mediaHost: MediaHost,
55     private val bypassController: KeyguardBypassController,
56     private val statusBarStateController: SysuiStatusBarStateController,
57     @ShadeDisplayAware private val context: Context,
58     @ShadeDisplayAware configurationController: ConfigurationController,
59     private val splitShadeStateController: SplitShadeStateController,
60     private val logger: KeyguardMediaControllerLogger,
61     dumpManager: DumpManager,
62 ) : Dumpable {
63     private var lastUsedStatusBarState = -1
64 
65     init {
66         dumpManager.registerDumpable(this)
67         statusBarStateController.addCallback(
68             object : StatusBarStateController.StateListener {
onStateChangednull69                 override fun onStateChanged(newState: Int) {
70                     refreshMediaPosition(reason = "StatusBarState.onStateChanged")
71                 }
72 
onDozingChangednull73                 override fun onDozingChanged(isDozing: Boolean) {
74                     refreshMediaPosition(reason = "StatusBarState.onDozingChanged")
75                 }
76             }
77         )
78         configurationController.addCallback(
79             object : ConfigurationController.ConfigurationListener {
onConfigChangednull80                 override fun onConfigChanged(newConfig: Configuration?) {
81                     updateResources()
82                 }
83             }
84         )
85 
86         // First let's set the desired state that we want for this host
87         mediaHost.expansion = MediaHostState.EXPANDED
88         mediaHost.showsOnlyActiveMedia = true
89         mediaHost.falsingProtectionNeeded = true
90 
91         // Let's now initialize this view, which also creates the host view for us.
92         mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
93         updateResources()
94     }
95 
updateResourcesnull96     private fun updateResources() {
97         useSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources)
98     }
99 
100     @VisibleForTesting
101     var useSplitShade = false
102         set(value) {
103             if (field == value) {
104                 return
105             }
106             field = value
107             reattachHostView()
108             refreshMediaPosition(reason = "useSplitShade changed")
109         }
110 
111     /** Is the media player visible? */
112     var visible = false
113         private set
114 
115     var visibilityChangedListener: ((Boolean) -> Unit)? = null
116 
117     /** single pane media container placed at the top of the notifications list */
118     var singlePaneContainer: MediaContainerView? = null
119         private set
120 
121     private var splitShadeContainer: ViewGroup? = null
122 
123     /**
124      * Attaches media container in single pane mode, situated at the top of the notifications list
125      */
attachSinglePaneContainernull126     fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
127         val needsListener = singlePaneContainer == null
128         singlePaneContainer = mediaView
129         if (needsListener) {
130             // On reinflation we don't want to add another listener
131             mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged)
132         }
133         reattachHostView()
134         onMediaHostVisibilityChanged(mediaHost.visible)
135 
136         singlePaneContainer?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
137     }
138 
139     /** Called whenever the media hosts visibility changes */
onMediaHostVisibilityChangednull140     private fun onMediaHostVisibilityChanged(visible: Boolean) {
141         refreshMediaPosition(reason = "onMediaHostVisibilityChanged")
142 
143         if (visible) {
144             if (useSplitShade) {
145                 return
146             }
147             mediaHost.hostView.layoutParams.apply {
148                 height = ViewGroup.LayoutParams.WRAP_CONTENT
149                 width = ViewGroup.LayoutParams.MATCH_PARENT
150             }
151         }
152     }
153 
154     /** Attaches media container in split shade mode, situated to the left of notifications */
attachSplitShadeContainernull155     fun attachSplitShadeContainer(container: ViewGroup) {
156         splitShadeContainer = container
157         reattachHostView()
158         refreshMediaPosition(reason = "attachSplitShadeContainer")
159     }
160 
reattachHostViewnull161     private fun reattachHostView() {
162         val inactiveContainer: ViewGroup?
163         val activeContainer: ViewGroup?
164         if (useSplitShade) {
165             activeContainer = splitShadeContainer
166             inactiveContainer = singlePaneContainer
167         } else {
168             inactiveContainer = splitShadeContainer
169             activeContainer = singlePaneContainer
170         }
171         if (inactiveContainer?.childCount == 1) {
172             inactiveContainer.removeAllViews()
173         }
174         if (activeContainer?.childCount == 0) {
175             // Detach the hostView from its parent view if exists
176             mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
177             activeContainer.addView(mediaHost.hostView)
178         }
179     }
180 
isWithinMediaViewBoundsnull181     fun isWithinMediaViewBounds(x: Int, y: Int): Boolean {
182         val bounds = Rect()
183         mediaHost.hostView.getBoundsOnScreen(bounds)
184 
185         return bounds.contains(x, y)
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         return !statusBarStateController.isDozing
236     }
237 
showMediaPlayernull238     private fun showMediaPlayer() {
239         if (useSplitShade) {
240             setVisibility(splitShadeContainer, View.VISIBLE)
241             setVisibility(singlePaneContainer, View.GONE)
242         } else {
243             setVisibility(singlePaneContainer, View.VISIBLE)
244             setVisibility(splitShadeContainer, View.GONE)
245         }
246     }
247 
hideMediaPlayernull248     private fun hideMediaPlayer() {
249         // always hide splitShadeContainer as it's initially visible and may influence layout
250         setVisibility(splitShadeContainer, View.GONE)
251         setVisibility(singlePaneContainer, View.GONE)
252     }
253 
setVisibilitynull254     private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
255         val currentMediaContainer = view ?: return
256 
257         val isVisible = newVisibility == View.VISIBLE
258 
259         if (currentMediaContainer is MediaContainerView) {
260             val previousVisibility = currentMediaContainer.visibility
261 
262             currentMediaContainer.setKeyguardVisibility(isVisible)
263             if (previousVisibility != newVisibility) {
264                 visibilityChangedListener?.invoke(isVisible)
265             }
266         } else {
267             currentMediaContainer.visibility = newVisibility
268         }
269     }
270 
dumpnull271     override fun dump(pw: PrintWriter, args: Array<out String>) {
272         pw.asIndenting().run {
273             println("KeyguardMediaController")
274             withIncreasedIndent {
275                 println("Self", this@KeyguardMediaController)
276                 println("visible", visible)
277                 println("useSplitShade", useSplitShade)
278                 println("bypassController.bypassEnabled", bypassController.bypassEnabled)
279                 println("singlePaneContainer", singlePaneContainer)
280                 println("splitShadeContainer", splitShadeContainer)
281                 if (lastUsedStatusBarState != -1) {
282                     println(
283                         "lastUsedStatusBarState",
284                         StatusBarState.toString(lastUsedStatusBarState),
285                     )
286                 }
287                 println(
288                     "statusBarStateController.state",
289                     StatusBarState.toString(statusBarStateController.state),
290                 )
291             }
292         }
293     }
294 
295     // This field is only used to log current active container.
296     private val activeContainer: ViewGroup?
297         get() = if (useSplitShade) splitShadeContainer else singlePaneContainer
298 }
299