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