• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.lifecycle.repeatWhenAttached
37 import com.android.systemui.people.ui.view.PeopleViewBinder.bind
38 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
39 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
40 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
41 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
42 import kotlin.math.roundToInt
43 import kotlinx.coroutines.flow.collect
44 import kotlinx.coroutines.launch
45 
46 /** A ViewBinder for [FooterActionsViewBinder]. */
47 object FooterActionsViewBinder {
48     /** Create a view that can later be [bound][bind] to a [FooterActionsViewModel]. */
49     @JvmStatic
50     fun create(context: Context): LinearLayout {
51         return LayoutInflater.from(context).inflate(R.layout.footer_actions, /* root= */ null)
52             as LinearLayout
53     }
54 
55     /** Bind [view] to [viewModel]. */
56     @JvmStatic
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         view.repeatWhenAttached {
102             val attachedScope = this.lifecycleScope
103 
104             attachedScope.launch {
105                 // Listen for dialog requests as soon as we are attached, even when not visible.
106                 // TODO(b/242040009): Should this move somewhere else?
107                 launch { viewModel.observeDeviceMonitoringDialogRequests(view.context) }
108 
109                 // Make sure we set the correct visibility and alpha even when QS are not currently
110                 // shown.
111                 launch {
112                     viewModel.isVisible.collect { isVisible -> view.isInvisible = !isVisible }
113                 }
114 
115                 launch { viewModel.alpha.collect { view.alpha = it } }
116                 launch {
117                     viewModel.backgroundAlpha.collect {
118                         view.background?.alpha = (it * 255).roundToInt()
119                     }
120                 }
121             }
122 
123             // Listen for model changes only when QS are visible.
124             qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
125                 // Security.
126                 launch {
127                     viewModel.security.collect { security ->
128                         if (previousSecurity != security) {
129                             bindSecurity(view.context, securityHolder, security)
130                             previousSecurity = security
131                         }
132                     }
133                 }
134 
135                 // Foreground services.
136                 launch {
137                     viewModel.foregroundServices.collect { foregroundServices ->
138                         if (previousForegroundServices != foregroundServices) {
139                             bindForegroundService(
140                                 foregroundServicesWithNumberHolder,
141                                 foregroundServicesWithTextHolder,
142                                 foregroundServices,
143                             )
144                             previousForegroundServices = foregroundServices
145                         }
146                     }
147                 }
148 
149                 // User switcher.
150                 launch {
151                     viewModel.userSwitcher.collect { userSwitcher ->
152                         if (previousUserSwitcher != userSwitcher) {
153                             bindButton(userSwitcherHolder, userSwitcher)
154                             previousUserSwitcher = userSwitcher
155                         }
156                     }
157                 }
158             }
159         }
160     }
161 
162     private fun bindSecurity(
163         quickSettingsContext: Context,
164         securityHolder: TextButtonViewHolder,
165         security: FooterActionsSecurityButtonViewModel?,
166     ) {
167         val securityView = securityHolder.view
168         securityView.isVisible = security != null
169         if (security == null) {
170             return
171         }
172 
173         // Make sure that the chevron is visible and that the button is clickable if there is a
174         // listener.
175         val chevron = securityHolder.chevron
176         val onClick = security.onClick
177         if (onClick != null) {
178             securityView.isClickable = true
179             securityView.setOnClickListener {
180                 onClick(quickSettingsContext, Expandable.fromView(securityView))
181             }
182             chevron.isVisible = true
183         } else {
184             securityView.isClickable = false
185             securityView.setOnClickListener(null)
186             chevron.isVisible = false
187         }
188 
189         securityHolder.text.text = security.text
190         securityHolder.newDot.isVisible = false
191         IconViewBinder.bind(security.icon, securityHolder.icon)
192     }
193 
194     private fun bindForegroundService(
195         foregroundServicesWithNumberHolder: NumberButtonViewHolder,
196         foregroundServicesWithTextHolder: TextButtonViewHolder,
197         foregroundServices: FooterActionsForegroundServicesButtonViewModel?,
198     ) {
199         val foregroundServicesWithNumberView = foregroundServicesWithNumberHolder.view
200         val foregroundServicesWithTextView = foregroundServicesWithTextHolder.view
201         if (foregroundServices == null) {
202             foregroundServicesWithNumberView.isVisible = false
203             foregroundServicesWithTextView.isVisible = false
204             return
205         }
206 
207         val foregroundServicesCount = foregroundServices.foregroundServicesCount
208         if (foregroundServices.displayText) {
209             // Button with text, icon and chevron.
210             foregroundServicesWithNumberView.isVisible = false
211 
212             foregroundServicesWithTextView.isVisible = true
213             foregroundServicesWithTextView.setOnClickListener {
214                 foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
215             }
216             foregroundServicesWithTextHolder.text.text = foregroundServices.text
217             foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges
218         } else {
219             // Small button with the number only.
220             foregroundServicesWithTextView.isVisible = false
221 
222             foregroundServicesWithNumberView.isVisible = true
223             foregroundServicesWithNumberView.setOnClickListener {
224                 foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView))
225             }
226             foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
227             foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
228             foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges
229         }
230     }
231 
232     private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) {
233         val buttonView = button.view
234         buttonView.id = model?.id ?: View.NO_ID
235         buttonView.isVisible = model != null
236         if (model == null) {
237             return
238         }
239 
240         val backgroundResource =
241             when (model.backgroundColor) {
242                 R.attr.offStateColor -> R.drawable.qs_footer_action_circle
243                 com.android.internal.R.attr.colorAccent -> R.drawable.qs_footer_action_circle_color
244                 else -> error("Unsupported icon background resource ${model.backgroundColor}")
245             }
246         buttonView.setBackgroundResource(backgroundResource)
247         buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
248 
249         val icon = model.icon
250         val iconView = button.icon
251 
252         IconViewBinder.bind(icon, iconView)
253         if (model.iconTint != null) {
254             iconView.setColorFilter(model.iconTint, PorterDuff.Mode.SRC_IN)
255         } else {
256             iconView.clearColorFilter()
257         }
258     }
259 }
260 
261 private class TextButtonViewHolder(val view: View) {
262     val icon = view.requireViewById<ImageView>(R.id.icon)
263     val text = view.requireViewById<TextView>(R.id.text)
264     val newDot = view.requireViewById<ImageView>(R.id.new_dot)
265     val chevron = view.requireViewById<ImageView>(R.id.chevron_icon)
266 
267     companion object {
createAndAddnull268         fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): TextButtonViewHolder {
269             val view =
270                 inflater.inflate(
271                     R.layout.footer_actions_text_button,
272                     /* root= */ root,
273                     /* attachToRoot= */ false,
274                 )
275             root.addView(view)
276             return TextButtonViewHolder(view)
277         }
278     }
279 }
280 
281 private class NumberButtonViewHolder(val view: View) {
282     val number = view.requireViewById<TextView>(R.id.number)
283     val newDot = view.requireViewById<ImageView>(R.id.new_dot)
284 
285     companion object {
createAndAddnull286         fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): NumberButtonViewHolder {
287             val view =
288                 inflater.inflate(
289                     R.layout.footer_actions_number_button,
290                     /* root= */ root,
291                     /* attachToRoot= */ false,
292                 )
293             root.addView(view)
294             return NumberButtonViewHolder(view)
295         }
296     }
297 }
298 
299 private class IconButtonViewHolder(val view: View) {
300     val icon = view.requireViewById<ImageView>(R.id.icon)
301 
302     companion object {
createAndAddnull303         fun createAndAdd(
304             inflater: LayoutInflater,
305             root: ViewGroup,
306             isLast: Boolean,
307         ): IconButtonViewHolder {
308             val view =
309                 inflater.inflate(
310                     R.layout.footer_actions_icon_button,
311                     /* root= */ root,
312                     /* attachToRoot= */ false,
313                 )
314 
315             // All buttons have a background with an inset of qs_footer_action_inset, so the last
316             // button must have a negative inset of -qs_footer_action_inset to compensate and be
317             // aligned with its parent.
318             val marginEnd =
319                 if (isLast) {
320                     -view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset)
321                 } else {
322                     0
323                 }
324 
325             val size =
326                 view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_button_size)
327             root.addView(
328                 view,
329                 LinearLayout.LayoutParams(size, size).apply { this.marginEnd = marginEnd },
330             )
331             return IconButtonViewHolder(view)
332         }
333     }
334 }
335