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