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