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 {
63                 override fun onStateChanged(newState: Int) {
64                     refreshMediaPosition()
65                 }
66 
67                 override fun onDozingChanged(isDozing: Boolean) {
68                     refreshMediaPosition()
69                 }
70             }
71         )
72         configurationController.addCallback(
73             object : ConfigurationController.ConfigurationListener {
74                 override fun onConfigChanged(newConfig: Configuration?) {
75                     updateResources()
76                 }
77             }
78         )
79 
80         val settingsObserver: ContentObserver =
81             object : ContentObserver(handler) {
82                 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 
110     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     /**
132      * Whether the doze wake up animation is delayed and we are currently waiting for it to start.
133      */
134     var isDozeWakeUpAnimationWaiting: Boolean = false
135         set(value) {
136             field = value
137             refreshMediaPosition()
138         }
139 
140     /** single pane media container placed at the top of the notifications list */
141     var singlePaneContainer: MediaContainerView? = null
142         private set
143     private var splitShadeContainer: ViewGroup? = null
144 
145     /** Track the media player setting status on lock screen. */
146     private var allowMediaPlayerOnLockScreen: Boolean =
147         secureSettings.getBoolForUser(
148             Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
149             true,
150             UserHandle.USER_CURRENT
151         )
152     private val lockScreenMediaPlayerUri =
153         secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
154 
155     /**
156      * Attaches media container in single pane mode, situated at the top of the notifications list
157      */
158     fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
159         val needsListener = singlePaneContainer == null
160         singlePaneContainer = mediaView
161         if (needsListener) {
162             // On reinflation we don't want to add another listener
163             mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged)
164         }
165         reattachHostView()
166         onMediaHostVisibilityChanged(mediaHost.visible)
167     }
168 
169     /** Called whenever the media hosts visibility changes */
170     private fun onMediaHostVisibilityChanged(visible: Boolean) {
171         refreshMediaPosition()
172         if (visible) {
173             mediaHost.hostView.layoutParams.apply {
174                 height = ViewGroup.LayoutParams.WRAP_CONTENT
175                 width = ViewGroup.LayoutParams.MATCH_PARENT
176             }
177         }
178     }
179 
180     /** Attaches media container in split shade mode, situated to the left of notifications */
181     fun attachSplitShadeContainer(container: ViewGroup) {
182         splitShadeContainer = container
183         reattachHostView()
184         refreshMediaPosition()
185     }
186 
187     private fun reattachHostView() {
188         val inactiveContainer: ViewGroup?
189         val activeContainer: ViewGroup?
190         if (useSplitShade) {
191             activeContainer = splitShadeContainer
192             inactiveContainer = singlePaneContainer
193         } else {
194             inactiveContainer = splitShadeContainer
195             activeContainer = singlePaneContainer
196         }
197         if (inactiveContainer?.childCount == 1) {
198             inactiveContainer.removeAllViews()
199         }
200         if (activeContainer?.childCount == 0) {
201             // Detach the hostView from its parent view if exists
202             mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
203             activeContainer.addView(mediaHost.hostView)
204         }
205     }
206 
207     fun refreshMediaPosition() {
208         val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD)
209         // mediaHost.visible required for proper animations handling
210         visible =
211             mediaHost.visible &&
212                 !bypassController.bypassEnabled &&
213                 keyguardOrUserSwitcher &&
214                 allowMediaPlayerOnLockScreen &&
215                 shouldBeVisibleForSplitShade()
216         if (visible) {
217             showMediaPlayer()
218         } else {
219             hideMediaPlayer()
220         }
221     }
222 
223     private fun shouldBeVisibleForSplitShade(): Boolean {
224         if (!useSplitShade) {
225             return true
226         }
227         // We have to explicitly hide media for split shade when on AOD, as it is a child view of
228         // keyguard status view, and nothing hides keyguard status view on AOD.
229         // When using the double-line clock, it is not an issue, as media gets implicitly hidden
230         // by the clock. This is not the case for single-line clock though.
231         // For single shade, we don't need to do it, because media is a child of NSSL, which already
232         // gets hidden on AOD.
233         // Media also has to be hidden when waking up from dozing, and the doze wake up animation is
234         // delayed and waiting to be started.
235         // This is to stay in sync with the delaying of the horizontal alignment of the rest of the
236         // keyguard container, that is also delayed until the "wait" is over.
237         // If we show media during this waiting period, the shade will still be centered, and using
238         // the entire width of the screen, and making media show fully stretched.
239         return !statusBarStateController.isDozing && !isDozeWakeUpAnimationWaiting
240     }
241 
242     private fun showMediaPlayer() {
243         if (useSplitShade) {
244             setVisibility(splitShadeContainer, View.VISIBLE)
245             setVisibility(singlePaneContainer, View.GONE)
246         } else {
247             setVisibility(singlePaneContainer, View.VISIBLE)
248             setVisibility(splitShadeContainer, View.GONE)
249         }
250     }
251 
252     private fun hideMediaPlayer() {
253         // always hide splitShadeContainer as it's initially visible and may influence layout
254         setVisibility(splitShadeContainer, View.GONE)
255         setVisibility(singlePaneContainer, View.GONE)
256     }
257 
258     private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
259         val previousVisibility = view?.visibility
260         view?.visibility = newVisibility
261         if (previousVisibility != newVisibility) {
262             visibilityChangedListener?.invoke(newVisibility == View.VISIBLE)
263         }
264     }
265 }
266