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 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.dagger.SysUISingleton 25 import com.android.systemui.media.dagger.MediaModule.KEYGUARD 26 import com.android.systemui.plugins.statusbar.StatusBarStateController 27 import com.android.systemui.statusbar.FeatureFlags 28 import com.android.systemui.statusbar.NotificationLockscreenUserManager 29 import com.android.systemui.statusbar.StatusBarState 30 import com.android.systemui.statusbar.SysuiStatusBarStateController 31 import com.android.systemui.statusbar.notification.stack.MediaHeaderView 32 import com.android.systemui.statusbar.phone.KeyguardBypassController 33 import com.android.systemui.statusbar.policy.ConfigurationController 34 import com.android.systemui.util.Utils 35 import javax.inject.Inject 36 import javax.inject.Named 37 38 /** 39 * Controls the media notifications on the lock screen, handles its visibility and placement - 40 * switches media player positioning between split pane container vs single pane container 41 */ 42 @SysUISingleton 43 class KeyguardMediaController @Inject constructor( 44 @param:Named(KEYGUARD) private val mediaHost: MediaHost, 45 private val bypassController: KeyguardBypassController, 46 private val statusBarStateController: SysuiStatusBarStateController, 47 private val notifLockscreenUserManager: NotificationLockscreenUserManager, 48 private val featureFlags: FeatureFlags, 49 private val context: Context, 50 configurationController: ConfigurationController 51 ) { 52 53 init { 54 statusBarStateController.addCallback(object : StatusBarStateController.StateListener { onStateChangednull55 override fun onStateChanged(newState: Int) { 56 refreshMediaPosition() 57 } 58 }) 59 configurationController.addCallback(object : ConfigurationController.ConfigurationListener { onConfigChangednull60 override fun onConfigChanged(newConfig: Configuration?) { 61 updateResources() 62 } 63 }) 64 65 // First let's set the desired state that we want for this host 66 mediaHost.expansion = MediaHostState.COLLAPSED 67 mediaHost.showsOnlyActiveMedia = true 68 mediaHost.falsingProtectionNeeded = true 69 70 // Let's now initialize this view, which also creates the host view for us. 71 mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN) 72 updateResources() 73 } 74 updateResourcesnull75 private fun updateResources() { 76 useSplitShade = Utils.shouldUseSplitNotificationShade(featureFlags, context.resources) 77 } 78 79 @VisibleForTesting 80 var useSplitShade = false 81 set(value) { 82 if (field == value) { 83 return 84 } 85 field = value 86 reattachHostView() 87 refreshMediaPosition() 88 } 89 90 /** 91 * Is the media player visible? 92 */ 93 var visible = false 94 private set 95 96 var visibilityChangedListener: ((Boolean) -> Unit)? = null 97 98 /** 99 * single pane media container placed at the top of the notifications list 100 */ 101 var singlePaneContainer: MediaHeaderView? = null 102 private set 103 private var splitShadeContainer: ViewGroup? = null 104 105 /** 106 * Attaches media container in single pane mode, situated at the top of the notifications list 107 */ attachSinglePaneContainernull108 fun attachSinglePaneContainer(mediaView: MediaHeaderView?) { 109 val needsListener = singlePaneContainer == null 110 singlePaneContainer = mediaView 111 if (needsListener) { 112 // On reinflation we don't want to add another listener 113 mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged) 114 } 115 reattachHostView() 116 onMediaHostVisibilityChanged(mediaHost.visible) 117 } 118 119 /** 120 * Called whenever the media hosts visibility changes 121 */ onMediaHostVisibilityChangednull122 private fun onMediaHostVisibilityChanged(visible: Boolean) { 123 refreshMediaPosition() 124 if (visible) { 125 mediaHost.hostView.layoutParams.apply { 126 height = ViewGroup.LayoutParams.WRAP_CONTENT 127 width = ViewGroup.LayoutParams.MATCH_PARENT 128 } 129 } 130 } 131 132 /** 133 * Attaches media container in split shade mode, situated to the left of notifications 134 */ attachSplitShadeContainernull135 fun attachSplitShadeContainer(container: ViewGroup) { 136 splitShadeContainer = container 137 reattachHostView() 138 refreshMediaPosition() 139 } 140 reattachHostViewnull141 private fun reattachHostView() { 142 val inactiveContainer: ViewGroup? 143 val activeContainer: ViewGroup? 144 if (useSplitShade) { 145 activeContainer = splitShadeContainer 146 inactiveContainer = singlePaneContainer 147 } else { 148 inactiveContainer = splitShadeContainer 149 activeContainer = singlePaneContainer 150 } 151 if (inactiveContainer?.childCount == 1) { 152 inactiveContainer.removeAllViews() 153 } 154 if (activeContainer?.childCount == 0) { 155 // Detach the hostView from its parent view if exists 156 mediaHost.hostView.parent?.let { 157 (it as? ViewGroup)?.removeView(mediaHost.hostView) 158 } 159 activeContainer.addView(mediaHost.hostView) 160 } 161 } 162 refreshMediaPositionnull163 fun refreshMediaPosition() { 164 val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD || 165 statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER) 166 // mediaHost.visible required for proper animations handling 167 visible = mediaHost.visible && 168 !bypassController.bypassEnabled && 169 keyguardOrUserSwitcher && 170 notifLockscreenUserManager.shouldShowLockscreenNotifications() 171 if (visible) { 172 showMediaPlayer() 173 } else { 174 hideMediaPlayer() 175 } 176 } 177 showMediaPlayernull178 private fun showMediaPlayer() { 179 if (useSplitShade) { 180 setVisibility(splitShadeContainer, View.VISIBLE) 181 setVisibility(singlePaneContainer, View.GONE) 182 } else { 183 setVisibility(singlePaneContainer, View.VISIBLE) 184 setVisibility(splitShadeContainer, View.GONE) 185 } 186 } 187 hideMediaPlayernull188 private fun hideMediaPlayer() { 189 // always hide splitShadeContainer as it's initially visible and may influence layout 190 setVisibility(splitShadeContainer, View.GONE) 191 setVisibility(singlePaneContainer, View.GONE) 192 } 193 setVisibilitynull194 private fun setVisibility(view: ViewGroup?, newVisibility: Int) { 195 val previousVisibility = view?.visibility 196 view?.visibility = newVisibility 197 if (previousVisibility != newVisibility) { 198 visibilityChangedListener?.invoke(newVisibility == View.VISIBLE) 199 } 200 } 201 }