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