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 }