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