• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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