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