1 /*
2  * Copyright (C) 2022 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.qs.footer.ui.binder
18 
19 import android.content.Context
20 import android.graphics.PorterDuff
21 import android.view.LayoutInflater
22 import android.view.View
23 import android.view.ViewGroup
24 import android.widget.ImageView
25 import android.widget.LinearLayout
26 import android.widget.TextView
27 import androidx.core.view.isInvisible
28 import androidx.core.view.isVisible
29 import androidx.lifecycle.Lifecycle
30 import androidx.lifecycle.LifecycleOwner
31 import androidx.lifecycle.lifecycleScope
32 import androidx.lifecycle.repeatOnLifecycle
33 import com.android.systemui.R
34 import com.android.systemui.animation.Expandable
35 import com.android.systemui.common.ui.binder.IconViewBinder
36 import com.android.systemui.dagger.SysUISingleton
37 import com.android.systemui.lifecycle.repeatWhenAttached
38 import com.android.systemui.people.ui.view.PeopleViewBinder.bind
39 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
40 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
41 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
42 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
43 import javax.inject.Inject
44 import kotlin.math.roundToInt
45 import kotlinx.coroutines.launch
46 
47 /** A ViewBinder for [FooterActionsViewBinder]. */
48 @SysUISingleton
49 class FooterActionsViewBinder @Inject constructor() {
50     /** Create a view that can later be [bound][bind] to a [FooterActionsViewModel]. */
51     fun create(context: Context): LinearLayout {
52         return LayoutInflater.from(context).inflate(R.layout.footer_actions, /* root= */ null)
53             as LinearLayout
54     }
55 
56     /** Bind [view] to [viewModel]. */
57     fun bind(
58         view: LinearLayout,
59         viewModel: FooterActionsViewModel,
60         qsVisibilityLifecycleOwner: LifecycleOwner,
61     ) {
62         view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
63 
64         // Add the views used by this new implementation.
65         val context = view.context
66         val inflater = LayoutInflater.from(context)
67 
68         val securityHolder = TextButtonViewHolder.createAndAdd(inflater, view)
69         val foregroundServicesWithTextHolder = TextButtonViewHolder.createAndAdd(inflater, view)
70         val foregroundServicesWithNumberHolder = NumberButtonViewHolder.createAndAdd(inflater, view)
71         val userSwitcherHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = false)
72         val settingsHolder =
73             IconButtonViewHolder.createAndAdd(inflater, view, isLast = viewModel.power == null)
74 
75         // Bind the static power and settings buttons.
76         bindButton(settingsHolder, viewModel.settings)
77 
78         if (viewModel.power != null) {
79             val powerHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = true)
80             bindButton(powerHolder, viewModel.power)
81         }
82 
83         // There are 2 lifecycle scopes we are using here:
84         //   1) The scope created by [repeatWhenAttached] when [view] is attached, and destroyed
85         //      when the [view] is detached. We use this as the parent scope for all our [viewModel]
86         //      state collection, given that we don't want to do any work when [view] is detached.
87         //   2) The scope owned by [lifecycleOwner], which should be RESUMED only when Quick
88         //      Settings are visible. We use this to make sure we collect UI state only when the
89         //      View is visible.
90         //
91         // Given that we start our collection when the Quick Settings become visible, which happens
92         // every time the user swipes down the shade, we remember our previous UI state already
93         // bound to the UI to avoid binding the same values over and over for nothing.
94 
95         // TODO(b/242040009): Look into using only a single scope.
96 
97         var previousSecurity: FooterActionsSecurityButtonViewModel? = null
98         var previousForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null
99         var previousUserSwitcher: FooterActionsButtonViewModel? = null
100 
101         // Set the initial visibility on the View directly so that we don't briefly show it for a
102         // few frames before [viewModel.isVisible] is collected.
103         view.isInvisible = !viewModel.isVisible.value
104 
105         // Listen for ViewModel updates when the View is attached.
106         view.repeatWhenAttached {
107             val attachedScope = this.lifecycleScope
108 
109             attachedScope.launch {
110                 // Listen for dialog requests as soon as we are attached, even when not visible.
111                 // TODO(b/242040009): Should this move somewhere else?
112                 launch { viewModel.observeDeviceMonitoringDialogRequests(view.context) }
113 
114                 // Make sure we set the correct visibility and alpha even when QS are not currently
115                 // shown.
116                 launch {
117                     viewModel.isVisible.collect { isVisible -> view.isInvisible = !isVisible }
118                 }
119 
120                 launch { viewModel.alpha.collect { view.alpha = it } }
121                 launch {
122                     viewModel.backgroundAlpha.collect {
123                         view.background?.alpha = (it * 255).roundToInt()
124                     }
125                 }
126             }
127 
128             // Listen for model changes only when QS are visible.
129             qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
130                 // Security.
131                 launch {
132                     viewModel.security.collect { security ->
133                         if (previousSecurity != security) {
134                             bindSecurity(view.context, securityHolder, security)
135                             previousSecurity = security
136                         }
137                     }
138                 }
139 
140                 // Foreground services.
141                 launch {
142                     viewModel.foregroundServices.collect { foregroundServices ->
143                         if (previousForegroundServices != foregroundServices) {
144                             bindForegroundService(
145                                 foregroundServicesWithNumberHolder,
146                                 foregroundServicesWithTextHolder,
147                                 foregroundServices,
148                             )
149                             previousForegroundServices = foregroundServices
150                         }
151                     }
152                 }
153 
154                 // User switcher.
155                 launch {
156                     viewModel.userSwitcher.collect { userSwitcher ->
157                         if (previousUserSwitcher != userSwitcher) {
158                             bindButton(userSwitcherHolder, userSwitcher)
159                             previousUserSwitcher = userSwitcher
160                         }
161                     }
162                 }
163             }
164         }
165     }
166 
167     private fun bindSecurity(
168         quickSettingsContext: Context,
169         securityHolder: TextButtonViewHolder,
170         security: FooterActionsSecurityButtonViewModel?,
171     ) {
172         val securityView = securityHolder.view
173         securityView.isVisible = security != null
174         if (security == null) {
175             return
176         }
177 
178         // Make sure that the chevron is visible and that the button is clickable if there is a
179         // listener.
180         val chevron = securityHolder.chevron
181         val onClick = security.onClick
182         if (onClick != null) {
183             securityView.isClickable = true
184             securityView.setOnClickListener {
185                 onClick(quickSettingsContext, Expandable.fromView(securityView))
186             }
187             chevron.isVisible = true
188         } else {
189             securityView.isClickable = false
190             securityView.setOnClickListener(null)
191             chevron.isVisible = false
192         }
193 
194         securityHolder.text.text = security.text
195         securityHolder.newDot.isVisible = false
196         IconViewBinder.bind(security.icon, securityHolder.icon)
197     }
198 
199     private fun bindForegroundService(
200         foregroundServicesWithNumberHolder: NumberButtonViewHolder,
201         foregroundServicesWithTextHolder: TextButtonViewHolder,
202         foregroundServices: FooterActionsForegroundServicesButtonViewModel?,
203     ) {
204         val foregroundServicesWithNumberView = foregroundServicesWithNumberHolder.view
205         val foregroundServicesWithTextView = foregroundServicesWithTextHolder.view
206         if (foregroundServices == null) {
207             foregroundServicesWithNumberView.isVisible = false
208             foregroundServicesWithTextView.isVisible = false
209             return
210         }
211 
212         val foregroundServicesCount = foregroundServices.foregroundServicesCount
213         if (foregroundServices.displayText) {
214             // Button with text, icon and chevron.
215             foregroundServicesWithNumberView.isVisible = false
216 
217             foregroundServicesWithTextView.isVisible = true
218             foregroundServicesWithTextView.setOnClickListener {
219                 foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
220             }
221             foregroundServicesWithTextHolder.text.text = foregroundServices.text
222             foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges
223         } else {
224             // Small button with the number only.
225             foregroundServicesWithTextView.isVisible = false
226 
227             foregroundServicesWithNumberView.isVisible = true
228             foregroundServicesWithNumberView.setOnClickListener {
229                 foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView))
230             }
231             foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
232             foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
233             foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges
234         }
235     }
236 
237     private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) {
238         val buttonView = button.view
239         buttonView.id = model?.id ?: View.NO_ID
240         buttonView.isVisible = model != null
241         if (model == null) {
242             return
243         }
244 
245         val backgroundResource =
246             when (model.backgroundColor) {
247                 R.attr.shadeInactive -> R.drawable.qs_footer_action_circle
248                 R.attr.shadeActive -> R.drawable.qs_footer_action_circle_color
249                 else -> error("Unsupported icon background resource ${model.backgroundColor}")
250             }
251         buttonView.setBackgroundResource(backgroundResource)
252         buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
253 
254         val icon = model.icon
255         val iconView = button.icon
256 
257         IconViewBinder.bind(icon, iconView)
258         if (model.iconTint != null) {
259             iconView.setColorFilter(model.iconTint, PorterDuff.Mode.SRC_IN)
260         } else {
261             iconView.clearColorFilter()
262         }
263     }
264 }
265 
266 private class TextButtonViewHolder(val view: View) {
267     val icon = view.requireViewById<ImageView>(R.id.icon)
268     val text = view.requireViewById<TextView>(R.id.text)
269     val newDot = view.requireViewById<ImageView>(R.id.new_dot)
270     val chevron = view.requireViewById<ImageView>(R.id.chevron_icon)
271 
272     companion object {
273         fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): TextButtonViewHolder {
274             val view =
275                 inflater.inflate(
276                     R.layout.footer_actions_text_button,
277                     /* root= */ root,
278                     /* attachToRoot= */ false,
279                 )
280             root.addView(view)
281             return TextButtonViewHolder(view)
282         }
283     }
284 }
285 
286 private class NumberButtonViewHolder(val view: View) {
287     val number = view.requireViewById<TextView>(R.id.number)
288     val newDot = view.requireViewById<ImageView>(R.id.new_dot)
289 
290     companion object {
291         fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): NumberButtonViewHolder {
292             val view =
293                 inflater.inflate(
294                     R.layout.footer_actions_number_button,
295                     /* root= */ root,
296                     /* attachToRoot= */ false,
297                 )
298             root.addView(view)
299             return NumberButtonViewHolder(view)
300         }
301     }
302 }
303 
304 private class IconButtonViewHolder(val view: View) {
305     val icon = view.requireViewById<ImageView>(R.id.icon)
306 
307     companion object {
308         fun createAndAdd(
309             inflater: LayoutInflater,
310             root: ViewGroup,
311             isLast: Boolean,
312         ): IconButtonViewHolder {
313             val view =
314                 inflater.inflate(
315                     R.layout.footer_actions_icon_button,
316                     /* root= */ root,
317                     /* attachToRoot= */ false,
318                 )
319 
320             // All buttons have a background with an inset of qs_footer_action_inset, so the last
321             // button must have a negative inset of -qs_footer_action_inset to compensate and be
322             // aligned with its parent.
323             val marginEnd =
324                 if (isLast) {
325                     -view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset)
326                 } else {
327                     0
328                 }
329 
330             val size =
331                 view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_button_size)
332             root.addView(
333                 view,
334                 LinearLayout.LayoutParams(size, size).apply { this.marginEnd = marginEnd },
335             )
336             return IconButtonViewHolder(view)
337         }
338     }
339 }
340