blob: 2bf102f724f4b67336138afe80a26eb7369abcca [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media
import android.content.Context
import android.content.res.Configuration
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.media.dagger.MediaModule.KEYGUARD
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.FeatureFlags
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.stack.MediaHeaderView
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.Utils
import javax.inject.Inject
import javax.inject.Named
/**
* Controls the media notifications on the lock screen, handles its visibility and placement -
* switches media player positioning between split pane container vs single pane container
*/
@SysUISingleton
class KeyguardMediaController @Inject constructor(
@param:Named(KEYGUARD) private val mediaHost: MediaHost,
private val bypassController: KeyguardBypassController,
private val statusBarStateController: SysuiStatusBarStateController,
private val notifLockscreenUserManager: NotificationLockscreenUserManager,
private val featureFlags: FeatureFlags,
private val context: Context,
configurationController: ConfigurationController
) {
init {
statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
override fun onStateChanged(newState: Int) {
refreshMediaPosition()
}
})
configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
updateResources()
}
})
// First let's set the desired state that we want for this host
mediaHost.expansion = MediaHostState.COLLAPSED
mediaHost.showsOnlyActiveMedia = true
mediaHost.falsingProtectionNeeded = true
// Let's now initialize this view, which also creates the host view for us.
mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
updateResources()
}
private fun updateResources() {
useSplitShade = Utils.shouldUseSplitNotificationShade(featureFlags, context.resources)
}
@VisibleForTesting
var useSplitShade = false
set(value) {
if (field == value) {
return
}
field = value
reattachHostView()
refreshMediaPosition()
}
/**
* Is the media player visible?
*/
var visible = false
private set
var visibilityChangedListener: ((Boolean) -> Unit)? = null
/**
* single pane media container placed at the top of the notifications list
*/
var singlePaneContainer: MediaHeaderView? = null
private set
private var splitShadeContainer: ViewGroup? = null
/**
* Attaches media container in single pane mode, situated at the top of the notifications list
*/
fun attachSinglePaneContainer(mediaView: MediaHeaderView?) {
val needsListener = singlePaneContainer == null
singlePaneContainer = mediaView
if (needsListener) {
// On reinflation we don't want to add another listener
mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged)
}
reattachHostView()
onMediaHostVisibilityChanged(mediaHost.visible)
}
/**
* Called whenever the media hosts visibility changes
*/
private fun onMediaHostVisibilityChanged(visible: Boolean) {
refreshMediaPosition()
if (visible) {
mediaHost.hostView.layoutParams.apply {
height = ViewGroup.LayoutParams.WRAP_CONTENT
width = ViewGroup.LayoutParams.MATCH_PARENT
}
}
}
/**
* Attaches media container in split shade mode, situated to the left of notifications
*/
fun attachSplitShadeContainer(container: ViewGroup) {
splitShadeContainer = container
reattachHostView()
refreshMediaPosition()
}
private fun reattachHostView() {
val inactiveContainer: ViewGroup?
val activeContainer: ViewGroup?
if (useSplitShade) {
activeContainer = splitShadeContainer
inactiveContainer = singlePaneContainer
} else {
inactiveContainer = splitShadeContainer
activeContainer = singlePaneContainer
}
if (inactiveContainer?.childCount == 1) {
inactiveContainer.removeAllViews()
}
if (activeContainer?.childCount == 0) {
// Detach the hostView from its parent view if exists
mediaHost.hostView.parent?.let {
(it as? ViewGroup)?.removeView(mediaHost.hostView)
}
activeContainer.addView(mediaHost.hostView)
}
}
fun refreshMediaPosition() {
val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD ||
statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER)
// mediaHost.visible required for proper animations handling
visible = mediaHost.visible &&
!bypassController.bypassEnabled &&
keyguardOrUserSwitcher &&
notifLockscreenUserManager.shouldShowLockscreenNotifications()
if (visible) {
showMediaPlayer()
} else {
hideMediaPlayer()
}
}
private fun showMediaPlayer() {
if (useSplitShade) {
setVisibility(splitShadeContainer, View.VISIBLE)
setVisibility(singlePaneContainer, View.GONE)
} else {
setVisibility(singlePaneContainer, View.VISIBLE)
setVisibility(splitShadeContainer, View.GONE)
}
}
private fun hideMediaPlayer() {
// always hide splitShadeContainer as it's initially visible and may influence layout
setVisibility(splitShadeContainer, View.GONE)
setVisibility(singlePaneContainer, View.GONE)
}
private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
val previousVisibility = view?.visibility
view?.visibility = newVisibility
if (previousVisibility != newVisibility) {
visibilityChangedListener?.invoke(newVisibility == View.VISIBLE)
}
}
}