• 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
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.database.ContentObserver
22 import android.net.Uri
23 import android.os.Handler
24 import android.os.UserHandle
25 import android.provider.Settings
26 import android.view.View
27 import android.view.ViewGroup
28 import androidx.annotation.VisibleForTesting
29 import com.android.systemui.dagger.SysUISingleton
30 import com.android.systemui.dagger.qualifiers.Main
31 import com.android.systemui.media.dagger.MediaModule.KEYGUARD
32 import com.android.systemui.plugins.statusbar.StatusBarStateController
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.util.LargeScreenUtils
39 import com.android.systemui.util.settings.SecureSettings
40 import javax.inject.Inject
41 import javax.inject.Named
42 
43 /**
44  * Controls the media notifications on the lock screen, handles its visibility and placement -
45  * switches media player positioning between split pane container vs single pane container
46  */
47 @SysUISingleton
48 class KeyguardMediaController
49 @Inject
50 constructor(
51     @param:Named(KEYGUARD) private val mediaHost: MediaHost,
52     private val bypassController: KeyguardBypassController,
53     private val statusBarStateController: SysuiStatusBarStateController,
54     private val context: Context,
55     private val secureSettings: SecureSettings,
56     @Main private val handler: Handler,
57     configurationController: ConfigurationController,
58 ) {
59 
60     init {
61         statusBarStateController.addCallback(
62             object : StatusBarStateController.StateListener {
onStateChangednull63                 override fun onStateChanged(newState: Int) {
64                     refreshMediaPosition()
65                 }
66 
onDozingChangednull67                 override fun onDozingChanged(isDozing: Boolean) {
68                     refreshMediaPosition()
69                 }
70             }
71         )
72         configurationController.addCallback(
73             object : ConfigurationController.ConfigurationListener {
onConfigChangednull74                 override fun onConfigChanged(newConfig: Configuration?) {
75                     updateResources()
76                 }
77             }
78         )
79 
80         val settingsObserver: ContentObserver =
81             object : ContentObserver(handler) {
onChangenull82                 override fun onChange(selfChange: Boolean, uri: Uri?) {
83                     if (uri == lockScreenMediaPlayerUri) {
84                         allowMediaPlayerOnLockScreen =
85                             secureSettings.getBoolForUser(
86                                 Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
87                                 true,
88                                 UserHandle.USER_CURRENT
89                             )
90                         refreshMediaPosition()
91                     }
92                 }
93             }
94         secureSettings.registerContentObserverForUser(
95             Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
96             settingsObserver,
97             UserHandle.USER_ALL
98         )
99 
100         // First let's set the desired state that we want for this host
101         mediaHost.expansion = MediaHostState.EXPANDED
102         mediaHost.showsOnlyActiveMedia = true
103         mediaHost.falsingProtectionNeeded = true
104 
105         // Let's now initialize this view, which also creates the host view for us.
106         mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
107         updateResources()
108     }
109 
updateResourcesnull110     private fun updateResources() {
111         useSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
112     }
113 
114     @VisibleForTesting
115     var useSplitShade = false
116         set(value) {
117             if (field == value) {
118                 return
119             }
120             field = value
121             reattachHostView()
122             refreshMediaPosition()
123         }
124 
125     /** Is the media player visible? */
126     var visible = false
127         private set
128 
129     var visibilityChangedListener: ((Boolean) -> Unit)? = null
130 
131     /** single pane media container placed at the top of the notifications list */
132     var singlePaneContainer: MediaContainerView? = null
133         private set
134     private var splitShadeContainer: ViewGroup? = null
135 
136     /** Track the media player setting status on lock screen. */
137     private var allowMediaPlayerOnLockScreen: Boolean =
138         secureSettings.getBoolForUser(
139             Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
140             true,
141             UserHandle.USER_CURRENT
142         )
143     private val lockScreenMediaPlayerUri =
144         secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
145 
146     /**
147      * Attaches media container in single pane mode, situated at the top of the notifications list
148      */
attachSinglePaneContainernull149     fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
150         val needsListener = singlePaneContainer == null
151         singlePaneContainer = mediaView
152         if (needsListener) {
153             // On reinflation we don't want to add another listener
154             mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged)
155         }
156         reattachHostView()
157         onMediaHostVisibilityChanged(mediaHost.visible)
158     }
159 
160     /** Called whenever the media hosts visibility changes */
onMediaHostVisibilityChangednull161     private fun onMediaHostVisibilityChanged(visible: Boolean) {
162         refreshMediaPosition()
163         if (visible) {
164             mediaHost.hostView.layoutParams.apply {
165                 height = ViewGroup.LayoutParams.WRAP_CONTENT
166                 width = ViewGroup.LayoutParams.MATCH_PARENT
167             }
168         }
169     }
170 
171     /** Attaches media container in split shade mode, situated to the left of notifications */
attachSplitShadeContainernull172     fun attachSplitShadeContainer(container: ViewGroup) {
173         splitShadeContainer = container
174         reattachHostView()
175         refreshMediaPosition()
176     }
177 
reattachHostViewnull178     private fun reattachHostView() {
179         val inactiveContainer: ViewGroup?
180         val activeContainer: ViewGroup?
181         if (useSplitShade) {
182             activeContainer = splitShadeContainer
183             inactiveContainer = singlePaneContainer
184         } else {
185             inactiveContainer = splitShadeContainer
186             activeContainer = singlePaneContainer
187         }
188         if (inactiveContainer?.childCount == 1) {
189             inactiveContainer.removeAllViews()
190         }
191         if (activeContainer?.childCount == 0) {
192             // Detach the hostView from its parent view if exists
193             mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
194             activeContainer.addView(mediaHost.hostView)
195         }
196     }
197 
refreshMediaPositionnull198     fun refreshMediaPosition() {
199         val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD)
200         // mediaHost.visible required for proper animations handling
201         visible =
202             mediaHost.visible &&
203                 !bypassController.bypassEnabled &&
204                 keyguardOrUserSwitcher &&
205                 allowMediaPlayerOnLockScreen &&
206                 shouldBeVisibleForSplitShade()
207         if (visible) {
208             showMediaPlayer()
209         } else {
210             hideMediaPlayer()
211         }
212     }
213 
shouldBeVisibleForSplitShadenull214     private fun shouldBeVisibleForSplitShade(): Boolean {
215         if (!useSplitShade) {
216             return true
217         }
218         // We have to explicitly hide media for split shade when on AOD, as it is a child view of
219         // keyguard status view, and nothing hides keyguard status view on AOD.
220         // When using the double-line clock, it is not an issue, as media gets implicitly hidden
221         // by the clock. This is not the case for single-line clock though.
222         // For single shade, we don't need to do it, because media is a child of NSSL, which already
223         // gets hidden on AOD.
224         return !statusBarStateController.isDozing
225     }
226 
showMediaPlayernull227     private fun showMediaPlayer() {
228         if (useSplitShade) {
229             setVisibility(splitShadeContainer, View.VISIBLE)
230             setVisibility(singlePaneContainer, View.GONE)
231         } else {
232             setVisibility(singlePaneContainer, View.VISIBLE)
233             setVisibility(splitShadeContainer, View.GONE)
234         }
235     }
236 
hideMediaPlayernull237     private fun hideMediaPlayer() {
238         // always hide splitShadeContainer as it's initially visible and may influence layout
239         setVisibility(splitShadeContainer, View.GONE)
240         setVisibility(singlePaneContainer, View.GONE)
241     }
242 
setVisibilitynull243     private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
244         val previousVisibility = view?.visibility
245         view?.visibility = newVisibility
246         if (previousVisibility != newVisibility) {
247             visibilityChangedListener?.invoke(newVisibility == View.VISIBLE)
248         }
249     }
250 }
251