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