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