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