| /* |
| * 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.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 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(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) |
| } |
| } |
| } |