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