1 /* <lambda>null2 * Copyright (C) 2021 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.shade 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.annotation.IdRes 22 import android.app.StatusBarManager 23 import android.content.res.Configuration 24 import android.os.Bundle 25 import android.os.Trace 26 import android.os.Trace.TRACE_TAG_APP 27 import android.util.Pair 28 import android.view.DisplayCutout 29 import android.view.View 30 import android.view.WindowInsets 31 import android.widget.TextView 32 import androidx.annotation.VisibleForTesting 33 import androidx.constraintlayout.motion.widget.MotionLayout 34 import com.android.settingslib.Utils 35 import com.android.systemui.Dumpable 36 import com.android.systemui.R 37 import com.android.systemui.animation.Interpolators 38 import com.android.systemui.animation.ShadeInterpolation 39 import com.android.systemui.battery.BatteryMeterView 40 import com.android.systemui.battery.BatteryMeterViewController 41 import com.android.systemui.demomode.DemoMode 42 import com.android.systemui.demomode.DemoModeController 43 import com.android.systemui.dump.DumpManager 44 import com.android.systemui.qs.ChipVisibilityListener 45 import com.android.systemui.qs.HeaderPrivacyIconsController 46 import com.android.systemui.qs.carrier.QSCarrierGroup 47 import com.android.systemui.qs.carrier.QSCarrierGroupController 48 import com.android.systemui.shade.ShadeHeaderController.Companion.HEADER_TRANSITION_ID 49 import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT 50 import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID 51 import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT 52 import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT 53 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider 54 import com.android.systemui.statusbar.phone.StatusBarIconController 55 import com.android.systemui.statusbar.phone.StatusBarLocation 56 import com.android.systemui.statusbar.phone.StatusIconContainer 57 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope 58 import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.SHADE_HEADER 59 import com.android.systemui.statusbar.policy.Clock 60 import com.android.systemui.statusbar.policy.ConfigurationController 61 import com.android.systemui.statusbar.policy.VariableDateView 62 import com.android.systemui.statusbar.policy.VariableDateViewController 63 import com.android.systemui.util.ViewController 64 import java.io.PrintWriter 65 import javax.inject.Inject 66 import javax.inject.Named 67 68 /** 69 * Controller for QS header. 70 * 71 * [header] is a [MotionLayout] that has two transitions: 72 * * [HEADER_TRANSITION_ID]: [QQS_HEADER_CONSTRAINT] <-> [QS_HEADER_CONSTRAINT] for portrait 73 * handheld device configuration. 74 * * [LARGE_SCREEN_HEADER_TRANSITION_ID]: [LARGE_SCREEN_HEADER_CONSTRAINT] for all other 75 * configurations 76 */ 77 @CentralSurfacesScope 78 class ShadeHeaderController 79 @Inject 80 constructor( 81 @Named(SHADE_HEADER) private val header: MotionLayout, 82 private val statusBarIconController: StatusBarIconController, 83 private val tintedIconManagerFactory: StatusBarIconController.TintedIconManager.Factory, 84 private val privacyIconsController: HeaderPrivacyIconsController, 85 private val insetsProvider: StatusBarContentInsetsProvider, 86 private val configurationController: ConfigurationController, 87 private val variableDateViewControllerFactory: VariableDateViewController.Factory, 88 @Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController, 89 private val dumpManager: DumpManager, 90 private val qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder, 91 private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager, 92 private val demoModeController: DemoModeController, 93 private val qsBatteryModeController: QsBatteryModeController, 94 ) : ViewController<View>(header), Dumpable { 95 96 companion object { 97 /** IDs for transitions and constraints for the [MotionLayout]. */ 98 @VisibleForTesting internal val HEADER_TRANSITION_ID = R.id.header_transition 99 @VisibleForTesting 100 internal val LARGE_SCREEN_HEADER_TRANSITION_ID = R.id.large_screen_header_transition 101 @VisibleForTesting internal val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint 102 @VisibleForTesting internal val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint 103 @VisibleForTesting 104 internal val LARGE_SCREEN_HEADER_CONSTRAINT = R.id.large_screen_header_constraint 105 106 private fun Int.stateToString() = 107 when (this) { 108 QQS_HEADER_CONSTRAINT -> "QQS Header" 109 QS_HEADER_CONSTRAINT -> "QS Header" 110 LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header" 111 else -> "Unknown state $this" 112 } 113 } 114 115 private lateinit var iconManager: StatusBarIconController.TintedIconManager 116 private lateinit var carrierIconSlots: List<String> 117 private lateinit var qsCarrierGroupController: QSCarrierGroupController 118 119 private val batteryIcon: BatteryMeterView = header.findViewById(R.id.batteryRemainingIcon) 120 private val clock: Clock = header.findViewById(R.id.clock) 121 private val date: TextView = header.findViewById(R.id.date) 122 private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons) 123 private val qsCarrierGroup: QSCarrierGroup = header.findViewById(R.id.carrier_group) 124 125 private var roundedCorners = 0 126 private var cutout: DisplayCutout? = null 127 private var lastInsets: WindowInsets? = null 128 129 private var qsDisabled = false 130 private var visible = false 131 set(value) { 132 if (field == value) { 133 return 134 } 135 field = value 136 updateListeners() 137 } 138 139 private var customizing = false 140 set(value) { 141 if (field != value) { 142 field = value 143 updateVisibility() 144 } 145 } 146 147 /** 148 * Whether the QQS/QS part of the shade is visible. This is particularly important in 149 * Lockscreen, as the shade is visible but QS is not. 150 */ 151 var qsVisible = false 152 set(value) { 153 if (field == value) { 154 return 155 } 156 field = value 157 onShadeExpandedChanged() 158 } 159 160 /** 161 * Whether we are in a configuration with large screen width. In this case, the header is a 162 * single line. 163 */ 164 var largeScreenActive = false 165 set(value) { 166 if (field == value) { 167 return 168 } 169 field = value 170 onHeaderStateChanged() 171 } 172 173 /** Expansion fraction of the QQS/QS shade. This is not the expansion between QQS <-> QS. */ 174 var shadeExpandedFraction = -1f 175 set(value) { 176 if (qsVisible && field != value) { 177 header.alpha = ShadeInterpolation.getContentAlpha(value) 178 field = value 179 } 180 } 181 182 /** Expansion fraction of the QQS <-> QS animation. */ 183 var qsExpandedFraction = -1f 184 set(value) { 185 if (visible && field != value) { 186 field = value 187 updatePosition() 188 } 189 } 190 191 /** Current scroll of QS. */ 192 var qsScrollY = 0 193 set(value) { 194 if (field != value) { 195 field = value 196 updateScrollY() 197 } 198 } 199 200 private val insetListener = 201 View.OnApplyWindowInsetsListener { view, insets -> 202 updateConstraintsForInsets(view as MotionLayout, insets) 203 lastInsets = WindowInsets(insets) 204 205 view.onApplyWindowInsets(insets) 206 } 207 208 private val demoModeReceiver = 209 object : DemoMode { 210 override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK) 211 override fun dispatchDemoCommand(command: String, args: Bundle) = 212 clock.dispatchDemoCommand(command, args) 213 override fun onDemoModeStarted() = clock.onDemoModeStarted() 214 override fun onDemoModeFinished() = clock.onDemoModeFinished() 215 } 216 217 private val chipVisibilityListener: ChipVisibilityListener = 218 object : ChipVisibilityListener { 219 override fun onChipVisibilityRefreshed(visible: Boolean) { 220 // If the privacy chip is visible, we hide the status icons and battery remaining 221 // icon, only in QQS. 222 val update = 223 combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(visible) 224 header.updateAllConstraints(update) 225 } 226 } 227 228 private val configurationControllerListener = 229 object : ConfigurationController.ConfigurationListener { 230 override fun onConfigChanged(newConfig: Configuration?) { 231 val left = 232 header.resources.getDimensionPixelSize( 233 R.dimen.large_screen_shade_header_left_padding 234 ) 235 header.setPadding( 236 left, 237 header.paddingTop, 238 header.paddingRight, 239 header.paddingBottom 240 ) 241 } 242 243 override fun onDensityOrFontScaleChanged() { 244 clock.setTextAppearance(R.style.TextAppearance_QS_Status) 245 date.setTextAppearance(R.style.TextAppearance_QS_Status) 246 qsCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers) 247 loadConstraints() 248 header.minHeight = 249 resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) 250 lastInsets?.let { updateConstraintsForInsets(header, it) } 251 updateResources() 252 } 253 } 254 255 override fun onInit() { 256 variableDateViewControllerFactory.create(date as VariableDateView).init() 257 batteryMeterViewController.init() 258 259 // battery settings same as in QS icons 260 batteryMeterViewController.ignoreTunerUpdates() 261 262 iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS) 263 iconManager.setTint( 264 Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary) 265 ) 266 267 carrierIconSlots = 268 listOf(header.context.getString(com.android.internal.R.string.status_bar_mobile)) 269 qsCarrierGroupController = 270 qsCarrierGroupControllerBuilder.setQSCarrierGroup(qsCarrierGroup).build() 271 272 privacyIconsController.onParentVisible() 273 } 274 275 override fun onViewAttached() { 276 privacyIconsController.chipVisibilityListener = chipVisibilityListener 277 updateVisibility() 278 updateTransition() 279 280 header.setOnApplyWindowInsetsListener(insetListener) 281 282 clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> 283 val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f 284 v.pivotX = newPivot 285 v.pivotY = v.height.toFloat() / 2 286 287 qsCarrierGroup.setPaddingRelative((v.width * v.scaleX).toInt(), 0, 0, 0) 288 } 289 290 dumpManager.registerDumpable(this) 291 configurationController.addCallback(configurationControllerListener) 292 demoModeController.addCallback(demoModeReceiver) 293 statusBarIconController.addIconGroup(iconManager) 294 } 295 296 override fun onViewDetached() { 297 privacyIconsController.chipVisibilityListener = null 298 dumpManager.unregisterDumpable(this::class.java.simpleName) 299 configurationController.removeCallback(configurationControllerListener) 300 demoModeController.removeCallback(demoModeReceiver) 301 statusBarIconController.removeIconGroup(iconManager) 302 } 303 304 fun disable(state1: Int, state2: Int, animate: Boolean) { 305 val disabled = state2 and StatusBarManager.DISABLE2_QUICK_SETTINGS != 0 306 if (disabled == qsDisabled) return 307 qsDisabled = disabled 308 updateVisibility() 309 } 310 311 fun startCustomizingAnimation(show: Boolean, duration: Long) { 312 header 313 .animate() 314 .setDuration(duration) 315 .alpha(if (show) 0f else 1f) 316 .setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN) 317 .setListener(CustomizerAnimationListener(show)) 318 .start() 319 } 320 321 private fun loadConstraints() { 322 // Use resources.getXml instead of passing the resource id due to bug b/205018300 323 header 324 .getConstraintSet(QQS_HEADER_CONSTRAINT) 325 .load(context, resources.getXml(R.xml.qqs_header)) 326 header 327 .getConstraintSet(QS_HEADER_CONSTRAINT) 328 .load(context, resources.getXml(R.xml.qs_header)) 329 header 330 .getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT) 331 .load(context, resources.getXml(R.xml.large_screen_shade_header)) 332 } 333 334 private fun updateConstraintsForInsets(view: MotionLayout, insets: WindowInsets) { 335 val cutout = insets.displayCutout.also { this.cutout = it } 336 337 val sbInsets: Pair<Int, Int> = insetsProvider.getStatusBarContentInsetsForCurrentRotation() 338 val cutoutLeft = sbInsets.first 339 val cutoutRight = sbInsets.second 340 val hasCornerCutout: Boolean = insetsProvider.currentRotationHasCornerCutout() 341 updateQQSPaddings() 342 // Set these guides as the left/right limits for content that lives in the top row, using 343 // cutoutLeft and cutoutRight 344 var changes = 345 combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints( 346 if (view.isLayoutRtl) cutoutRight else cutoutLeft, 347 header.paddingStart, 348 if (view.isLayoutRtl) cutoutLeft else cutoutRight, 349 header.paddingEnd 350 ) 351 352 if (cutout != null) { 353 val topCutout = cutout.boundingRectTop 354 if (topCutout.isEmpty || hasCornerCutout) { 355 changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints() 356 } else { 357 changes += 358 combinedShadeHeadersConstraintManager.centerCutoutConstraints( 359 view.isLayoutRtl, 360 (view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2 361 ) 362 } 363 } else { 364 changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints() 365 } 366 367 view.updateAllConstraints(changes) 368 updateBatteryMode() 369 } 370 371 private fun updateBatteryMode() { 372 qsBatteryModeController.getBatteryMode(cutout, qsExpandedFraction)?.let { 373 batteryIcon.setPercentShowMode(it) 374 } 375 } 376 377 private fun updateScrollY() { 378 if (!largeScreenActive) { 379 header.scrollY = qsScrollY 380 } 381 } 382 383 private fun onShadeExpandedChanged() { 384 if (qsVisible) { 385 privacyIconsController.startListening() 386 } else { 387 privacyIconsController.stopListening() 388 } 389 updateVisibility() 390 updatePosition() 391 } 392 393 private fun onHeaderStateChanged() { 394 updateTransition() 395 } 396 397 /** 398 * If not using [combinedHeaders] this should only be visible on large screen. Else, it should 399 * be visible any time the QQS/QS shade is open. 400 */ 401 private fun updateVisibility() { 402 val visibility = 403 if (qsDisabled) { 404 View.GONE 405 } else if (qsVisible && !customizing) { 406 View.VISIBLE 407 } else { 408 View.INVISIBLE 409 } 410 if (header.visibility != visibility) { 411 header.visibility = visibility 412 visible = visibility == View.VISIBLE 413 } 414 } 415 416 private fun updateTransition() { 417 if (largeScreenActive) { 418 logInstantEvent("Large screen constraints set") 419 header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID) 420 } else { 421 logInstantEvent("Small screen constraints set") 422 header.setTransition(HEADER_TRANSITION_ID) 423 } 424 header.jumpToState(header.startState) 425 updatePosition() 426 updateScrollY() 427 } 428 429 private fun updatePosition() { 430 if (!largeScreenActive && visible) { 431 logInstantEvent("updatePosition: $qsExpandedFraction") 432 header.progress = qsExpandedFraction 433 updateBatteryMode() 434 } 435 } 436 437 private fun logInstantEvent(message: String) { 438 Trace.instantForTrack(TRACE_TAG_APP, "LargeScreenHeaderController", message) 439 } 440 441 private fun updateListeners() { 442 qsCarrierGroupController.setListening(visible) 443 if (visible) { 444 updateSingleCarrier(qsCarrierGroupController.isSingleCarrier) 445 qsCarrierGroupController.setOnSingleCarrierChangedListener { updateSingleCarrier(it) } 446 } else { 447 qsCarrierGroupController.setOnSingleCarrierChangedListener(null) 448 } 449 } 450 451 private fun updateSingleCarrier(singleCarrier: Boolean) { 452 if (singleCarrier) { 453 iconContainer.removeIgnoredSlots(carrierIconSlots) 454 } else { 455 iconContainer.addIgnoredSlots(carrierIconSlots) 456 } 457 } 458 459 private fun updateResources() { 460 roundedCorners = resources.getDimensionPixelSize(R.dimen.rounded_corner_content_padding) 461 val padding = resources.getDimensionPixelSize(R.dimen.qs_panel_padding) 462 header.setPadding(padding, header.paddingTop, padding, header.paddingBottom) 463 updateQQSPaddings() 464 qsBatteryModeController.updateResources() 465 } 466 467 private fun updateQQSPaddings() { 468 val clockPaddingStart = 469 resources.getDimensionPixelSize(R.dimen.status_bar_left_clock_starting_padding) 470 val clockPaddingEnd = 471 resources.getDimensionPixelSize(R.dimen.status_bar_left_clock_end_padding) 472 clock.setPaddingRelative( 473 clockPaddingStart, 474 clock.paddingTop, 475 clockPaddingEnd, 476 clock.paddingBottom 477 ) 478 } 479 480 override fun dump(pw: PrintWriter, args: Array<out String>) { 481 pw.println("visible: $visible") 482 pw.println("shadeExpanded: $qsVisible") 483 pw.println("shadeExpandedFraction: $shadeExpandedFraction") 484 pw.println("active: $largeScreenActive") 485 pw.println("qsExpandedFraction: $qsExpandedFraction") 486 pw.println("qsScrollY: $qsScrollY") 487 pw.println("currentState: ${header.currentState.stateToString()}") 488 } 489 490 private fun MotionLayout.updateConstraints(@IdRes state: Int, update: ConstraintChange) { 491 val constraints = getConstraintSet(state) 492 constraints.update() 493 updateState(state, constraints) 494 } 495 496 /** 497 * Updates the [ConstraintSet] for the case of combined headers. 498 * 499 * Only non-`null` changes are applied to reduce the number of rebuilding in the [MotionLayout]. 500 */ 501 private fun MotionLayout.updateAllConstraints(updates: ConstraintsChanges) { 502 if (updates.qqsConstraintsChanges != null) { 503 updateConstraints(QQS_HEADER_CONSTRAINT, updates.qqsConstraintsChanges) 504 } 505 if (updates.qsConstraintsChanges != null) { 506 updateConstraints(QS_HEADER_CONSTRAINT, updates.qsConstraintsChanges) 507 } 508 if (updates.largeScreenConstraintsChanges != null) { 509 updateConstraints(LARGE_SCREEN_HEADER_CONSTRAINT, updates.largeScreenConstraintsChanges) 510 } 511 } 512 513 @VisibleForTesting internal fun simulateViewDetached() = this.onViewDetached() 514 515 inner class CustomizerAnimationListener( 516 private val enteringCustomizing: Boolean, 517 ) : AnimatorListenerAdapter() { 518 override fun onAnimationEnd(animation: Animator?) { 519 super.onAnimationEnd(animation) 520 header.animate().setListener(null) 521 if (enteringCustomizing) { 522 customizing = true 523 } 524 } 525 526 override fun onAnimationStart(animation: Animator?) { 527 super.onAnimationStart(animation) 528 if (!enteringCustomizing) { 529 customizing = false 530 } 531 } 532 } 533 } 534