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