• 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.statusbar.events
18 
19 import android.animation.Animator
20 import android.annotation.UiThread
21 import android.graphics.Point
22 import android.graphics.Rect
23 import android.util.Log
24 import android.view.Gravity
25 import android.view.View
26 import android.widget.FrameLayout
27 
28 import com.android.internal.annotations.GuardedBy
29 import com.android.systemui.animation.Interpolators
30 import com.android.systemui.R
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.dagger.qualifiers.Main
33 import com.android.systemui.plugins.statusbar.StatusBarStateController
34 import com.android.systemui.statusbar.StatusBarState.SHADE
35 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
36 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
37 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
38 import com.android.systemui.statusbar.policy.ConfigurationController
39 import com.android.systemui.util.concurrency.DelayableExecutor
40 import com.android.systemui.util.leak.RotationUtils
41 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
42 import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
43 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
44 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
45 import com.android.systemui.util.leak.RotationUtils.Rotation
46 
47 import java.lang.IllegalStateException
48 import java.util.concurrent.Executor
49 import javax.inject.Inject
50 
51 /**
52  * Understands how to keep the persistent privacy dot in the corner of the screen in
53  * ScreenDecorations, which does not rotate with the device.
54  *
55  * The basic principle here is that each dot will sit in a box that is equal to the margins of the
56  * status bar (specifically the status_bar_contents view in PhoneStatusBarView). Each dot container
57  * will have its gravity set towards the corner (i.e., top-right corner gets top|right gravity), and
58  * the contained ImageView will be set to center_vertical and away from the corner horizontally. The
59  * Views will match the status bar top padding and status bar height so that the dot can appear to
60  * reside directly after the status bar system contents (basically after the battery).
61  *
62  * NOTE: any operation that modifies views directly must run on the provided executor, because
63  * these views are owned by ScreenDecorations and it runs in its own thread
64  */
65 
66 @SysUISingleton
67 class PrivacyDotViewController @Inject constructor(
68     @Main private val mainExecutor: Executor,
69     private val stateController: StatusBarStateController,
70     private val configurationController: ConfigurationController,
71     private val contentInsetsProvider: StatusBarContentInsetsProvider,
72     private val animationScheduler: SystemStatusAnimationScheduler
73 ) {
74     private var sbHeightPortrait = 0
75     private var sbHeightLandscape = 0
76 
77     private lateinit var tl: View
78     private lateinit var tr: View
79     private lateinit var bl: View
80     private lateinit var br: View
81 
82     // Only can be modified on @UiThread
83     private var currentViewState: ViewState = ViewState()
84 
85     @GuardedBy("lock")
86     private var nextViewState: ViewState = currentViewState.copy()
87         set(value) {
88             field = value
89             scheduleUpdate()
90         }
91     private val lock = Object()
92     private var cancelRunnable: Runnable? = null
93 
94     // Privacy dots are created in ScreenDecoration's UiThread, which is not the main thread
95     private var uiExecutor: DelayableExecutor? = null
96 
97     private val views: Sequence<View>
98         get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl)
99 
100     init {
101         contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener {
102             override fun onStatusBarContentInsetsChanged() {
103                 dlog("onStatusBarContentInsetsChanged: ")
104                 setNewLayoutRects()
105             }
106         })
107         configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
108             override fun onLayoutDirectionChanged(isRtl: Boolean) {
109                 synchronized(this) {
110                     val corner = selectDesignatedCorner(nextViewState.rotation, isRtl)
111                     nextViewState = nextViewState.copy(
112                             layoutRtl = isRtl,
113                             designatedCorner = corner
114                     )
115                 }
116             }
117         })
118 
119         stateController.addCallback(object : StatusBarStateController.StateListener {
120             override fun onExpandedChanged(isExpanded: Boolean) {
121                 updateStatusBarState()
122             }
123 
124             override fun onStateChanged(newState: Int) {
125                 updateStatusBarState()
126             }
127         })
128     }
129 
130     fun setUiExecutor(e: DelayableExecutor) {
131         uiExecutor = e
132     }
133 
134     fun setQsExpanded(expanded: Boolean) {
135         dlog("setQsExpanded $expanded")
136         synchronized(lock) {
137             nextViewState = nextViewState.copy(qsExpanded = expanded)
138         }
139     }
140 
141     @UiThread
142     fun setNewRotation(rot: Int) {
143         dlog("updateRotation: $rot")
144 
145         val isRtl: Boolean
146         synchronized(lock) {
147             if (rot == nextViewState.rotation) {
148                 return
149             }
150 
151             isRtl = nextViewState.layoutRtl
152         }
153 
154         // If we rotated, hide all dotes until the next state resolves
155         setCornerVisibilities(View.INVISIBLE)
156 
157         val newCorner = selectDesignatedCorner(rot, isRtl)
158         val index = newCorner.cornerIndex()
159 
160         val h = when (rot) {
161             0, 2 -> sbHeightPortrait
162             1, 3 -> sbHeightLandscape
163             else -> 0
164         }
165         synchronized(lock) {
166             nextViewState = nextViewState.copy(
167                     rotation = rot,
168                     height = h,
169                     designatedCorner = newCorner,
170                     cornerIndex = index)
171         }
172     }
173 
174     @UiThread
175     private fun hideDotView(dot: View, animate: Boolean) {
176         dot.clearAnimation()
177         if (animate) {
178             dot.animate()
179                     .setDuration(DURATION)
180                     .setInterpolator(Interpolators.ALPHA_OUT)
181                     .alpha(0f)
182                     .withEndAction { dot.visibility = View.INVISIBLE }
183                     .start()
184         } else {
185             dot.visibility = View.INVISIBLE
186         }
187     }
188 
189     @UiThread
190     private fun showDotView(dot: View, animate: Boolean) {
191         dot.clearAnimation()
192         if (animate) {
193             dot.visibility = View.VISIBLE
194             dot.alpha = 0f
195             dot.animate()
196                     .alpha(1f)
197                     .setDuration(DURATION)
198                     .setInterpolator(Interpolators.ALPHA_IN)
199                     .start()
200         } else {
201             dot.visibility = View.VISIBLE
202             dot.alpha = 1f
203         }
204     }
205 
206     @UiThread
207     private fun updateHeights(rot: Int) {
208         val height = when (rot) {
209             0, 2 -> sbHeightPortrait
210             1, 3 -> sbHeightLandscape
211             else -> 0
212         }
213 
214         views.forEach { it.layoutParams.height = height }
215     }
216 
217     // Update the gravity and margins of the privacy views
218     @UiThread
219     private fun updateRotations(rotation: Int) {
220         // To keep a view in the corner, its gravity is always the description of its current corner
221         // Therefore, just figure out which view is in which corner. This turns out to be something
222         // like (myCorner - rot) mod 4, where topLeft = 0, topRight = 1, etc. and portrait = 0, and
223         // rotating the device counter-clockwise increments rotation by 1
224 
225         views.forEach { corner ->
226             val rotatedCorner = rotatedCorner(cornerForView(corner), rotation)
227             (corner.layoutParams as FrameLayout.LayoutParams).apply {
228                 gravity = rotatedCorner.toGravity()
229             }
230 
231             // Set the dot's view gravity to hug the status bar
232             (corner.findViewById<View>(R.id.privacy_dot)
233                     .layoutParams as FrameLayout.LayoutParams)
234                         .gravity = rotatedCorner.innerGravity()
235         }
236     }
237 
238     @UiThread
239     private fun updateCornerSizes(l: Int, r: Int, rotation: Int) {
240         views.forEach { corner ->
241             val rotatedCorner = rotatedCorner(cornerForView(corner), rotation)
242             val w = widthForCorner(rotatedCorner, l, r)
243             (corner.layoutParams as FrameLayout.LayoutParams).width = w
244         }
245     }
246 
247     @UiThread
248     private fun setCornerSizes(state: ViewState) {
249         // StatusBarContentInsetsProvider can tell us the location of the privacy indicator dot
250         // in every rotation. The only thing we need to check is rtl
251         val rtl = state.layoutRtl
252         val size = Point()
253         tl.context.display.getRealSize(size)
254         val currentRotation = RotationUtils.getExactRotation(tl.context)
255 
256         val displayWidth: Int
257         val displayHeight: Int
258         if (currentRotation == ROTATION_LANDSCAPE || currentRotation == ROTATION_SEASCAPE) {
259             displayWidth = size.y
260             displayHeight = size.x
261         } else {
262             displayWidth = size.x
263             displayHeight = size.y
264         }
265 
266         var rot = activeRotationForCorner(tl, rtl)
267         var contentInsets = state.contentRectForRotation(rot)
268         (tl.layoutParams as FrameLayout.LayoutParams).apply {
269             height = contentInsets.height()
270             if (rtl) {
271                 width = contentInsets.left
272             } else {
273                 width = displayHeight - contentInsets.right
274             }
275         }
276 
277         rot = activeRotationForCorner(tr, rtl)
278         contentInsets = state.contentRectForRotation(rot)
279         (tr.layoutParams as FrameLayout.LayoutParams).apply {
280             height = contentInsets.height()
281             if (rtl) {
282                 width = contentInsets.left
283             } else {
284                 width = displayWidth - contentInsets.right
285             }
286         }
287 
288         rot = activeRotationForCorner(br, rtl)
289         contentInsets = state.contentRectForRotation(rot)
290         (br.layoutParams as FrameLayout.LayoutParams).apply {
291             height = contentInsets.height()
292             if (rtl) {
293                 width = contentInsets.left
294             } else {
295                 width = displayHeight - contentInsets.right
296             }
297         }
298 
299         rot = activeRotationForCorner(bl, rtl)
300         contentInsets = state.contentRectForRotation(rot)
301         (bl.layoutParams as FrameLayout.LayoutParams).apply {
302             height = contentInsets.height()
303             if (rtl) {
304                 width = contentInsets.left
305             } else {
306                 width = displayWidth - contentInsets.right
307             }
308         }
309     }
310 
311     // Designated view will be the one at statusbar's view.END
312     @UiThread
313     private fun selectDesignatedCorner(r: Int, isRtl: Boolean): View? {
314         if (!this::tl.isInitialized) {
315             return null
316         }
317 
318         return when (r) {
319             0 -> if (isRtl) tl else tr
320             1 -> if (isRtl) tr else br
321             2 -> if (isRtl) br else bl
322             3 -> if (isRtl) bl else tl
323             else -> throw IllegalStateException("unknown rotation")
324         }
325     }
326 
327     // Track the current designated corner and maybe animate to a new rotation
328     @UiThread
329     private fun updateDesignatedCorner(newCorner: View?, shouldShowDot: Boolean) {
330         if (shouldShowDot) {
331             newCorner?.apply {
332                 clearAnimation()
333                 visibility = View.VISIBLE
334                 alpha = 0f
335                 animate()
336                     .alpha(1.0f)
337                     .setDuration(300)
338                     .start()
339             }
340         }
341     }
342 
343     @UiThread
344     private fun setCornerVisibilities(vis: Int) {
345         views.forEach { corner ->
346             corner.visibility = vis
347         }
348     }
349 
350     private fun cornerForView(v: View): Int {
351         return when (v) {
352             tl -> TOP_LEFT
353             tr -> TOP_RIGHT
354             bl -> BOTTOM_LEFT
355             br -> BOTTOM_RIGHT
356             else -> throw IllegalArgumentException("not a corner view")
357         }
358     }
359 
360     private fun rotatedCorner(corner: Int, rotation: Int): Int {
361         var modded = corner - rotation
362         if (modded < 0) {
363             modded += 4
364         }
365 
366         return modded
367     }
368 
369     @Rotation
370     private fun activeRotationForCorner(corner: View, rtl: Boolean): Int {
371         // Each corner will only be visible in a single rotation, based on rtl
372         return when (corner) {
373             tr -> if (rtl) ROTATION_LANDSCAPE else ROTATION_NONE
374             tl -> if (rtl) ROTATION_NONE else ROTATION_SEASCAPE
375             br -> if (rtl) ROTATION_UPSIDE_DOWN else ROTATION_LANDSCAPE
376             else /* bl */ -> if (rtl) ROTATION_SEASCAPE else ROTATION_UPSIDE_DOWN
377         }
378     }
379 
380     private fun widthForCorner(corner: Int, left: Int, right: Int): Int {
381         return when (corner) {
382             TOP_LEFT, BOTTOM_LEFT -> left
383             TOP_RIGHT, BOTTOM_RIGHT -> right
384             else -> throw IllegalArgumentException("Unknown corner")
385         }
386     }
387 
388     fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) {
389         if (this::tl.isInitialized && this::tr.isInitialized &&
390                 this::bl.isInitialized && this::br.isInitialized) {
391             if (tl == topLeft && tr == topRight && bl == bottomLeft && br == bottomRight) {
392                 return
393             }
394         }
395 
396         tl = topLeft
397         tr = topRight
398         bl = bottomLeft
399         br = bottomRight
400 
401         val rtl = configurationController.isLayoutRtl
402         val dc = selectDesignatedCorner(0, rtl)
403 
404         val index = dc.cornerIndex()
405 
406         mainExecutor.execute {
407             animationScheduler.addCallback(systemStatusAnimationCallback)
408         }
409 
410         val left = contentInsetsProvider.getStatusBarContentInsetsForRotation(ROTATION_SEASCAPE)
411         val top = contentInsetsProvider.getStatusBarContentInsetsForRotation(ROTATION_NONE)
412         val right = contentInsetsProvider.getStatusBarContentInsetsForRotation(ROTATION_LANDSCAPE)
413         val bottom = contentInsetsProvider
414                 .getStatusBarContentInsetsForRotation(ROTATION_UPSIDE_DOWN)
415 
416         synchronized(lock) {
417             nextViewState = nextViewState.copy(
418                     viewInitialized = true,
419                     designatedCorner = dc,
420                     cornerIndex = index,
421                     seascapeRect = left,
422                     portraitRect = top,
423                     landscapeRect = right,
424                     upsideDownRect = bottom,
425                     layoutRtl = rtl
426             )
427         }
428     }
429 
430     /**
431      * Set the status bar height in portrait and landscape, in pixels. If they are the same you can
432      * pass the same value twice
433      */
434     fun setStatusBarHeights(portrait: Int, landscape: Int) {
435         sbHeightPortrait = portrait
436         sbHeightLandscape = landscape
437     }
438 
439     private fun updateStatusBarState() {
440         synchronized(lock) {
441             nextViewState = nextViewState.copy(shadeExpanded = isShadeInQs())
442         }
443     }
444 
445     /**
446      * If we are unlocked with an expanded shade, QS is showing. On keyguard, the shade is always
447      * expanded so we use other signals from the panel view controller to know if QS is expanded
448      */
449     @GuardedBy("lock")
450     private fun isShadeInQs(): Boolean {
451         return (stateController.isExpanded && stateController.state == SHADE) ||
452                 (stateController.state == SHADE_LOCKED)
453     }
454 
455     private fun scheduleUpdate() {
456         dlog("scheduleUpdate: ")
457 
458         cancelRunnable?.run()
459         cancelRunnable = uiExecutor?.executeDelayed({
460             processNextViewState()
461         }, 100)
462     }
463 
464     @UiThread
465     private fun processNextViewState() {
466         dlog("processNextViewState: ")
467 
468         val newState: ViewState
469         synchronized(lock) {
470             newState = nextViewState.copy()
471         }
472 
473         resolveState(newState)
474     }
475 
476     @UiThread
477     private fun resolveState(state: ViewState) {
478         dlog("resolveState $state")
479         if (!state.viewInitialized) {
480             dlog("resolveState: view is not initialized. skipping.")
481             return
482         }
483 
484         if (state == currentViewState) {
485             dlog("resolveState: skipping")
486             return
487         }
488 
489         if (state.rotation != currentViewState.rotation) {
490             // A rotation has started, hide the views to avoid flicker
491             updateRotations(state.rotation)
492         }
493 
494         if (state.needsLayout(currentViewState)) {
495             setCornerSizes(state)
496             views.forEach { it.requestLayout() }
497         }
498 
499         if (state.designatedCorner != currentViewState.designatedCorner) {
500             currentViewState.designatedCorner?.contentDescription = null
501             state.designatedCorner?.contentDescription = state.contentDescription
502 
503             updateDesignatedCorner(state.designatedCorner, state.shouldShowDot())
504         } else if (state.contentDescription != currentViewState.contentDescription) {
505             state.designatedCorner?.contentDescription = state.contentDescription
506         }
507 
508         val shouldShow = state.shouldShowDot()
509         if (shouldShow != currentViewState.shouldShowDot()) {
510             if (shouldShow && state.designatedCorner != null) {
511                 showDotView(state.designatedCorner, true)
512             } else if (!shouldShow && state.designatedCorner != null) {
513                 hideDotView(state.designatedCorner, true)
514             }
515         }
516 
517         currentViewState = state
518     }
519 
520     private val systemStatusAnimationCallback: SystemStatusAnimationCallback =
521             object : SystemStatusAnimationCallback {
522         override fun onSystemStatusAnimationTransitionToPersistentDot(
523             contentDescr: String?
524         ): Animator? {
525             synchronized(lock) {
526                 nextViewState = nextViewState.copy(
527                         systemPrivacyEventIsActive = true,
528                         contentDescription = contentDescr)
529             }
530 
531             return null
532         }
533 
534         override fun onHidePersistentDot(): Animator? {
535             synchronized(lock) {
536                 nextViewState = nextViewState.copy(systemPrivacyEventIsActive = false)
537             }
538 
539             return null
540         }
541     }
542 
543     private fun View?.cornerIndex(): Int {
544         if (this != null) {
545             return cornerForView(this)
546         }
547         return -1
548     }
549 
550     // Returns [left, top, right, bottom] aka [seascape, none, landscape, upside-down]
551     private fun getLayoutRects(): List<Rect> {
552         val left = contentInsetsProvider.getStatusBarContentInsetsForRotation(ROTATION_SEASCAPE)
553         val top = contentInsetsProvider.getStatusBarContentInsetsForRotation(ROTATION_NONE)
554         val right = contentInsetsProvider.getStatusBarContentInsetsForRotation(ROTATION_LANDSCAPE)
555         val bottom = contentInsetsProvider
556                 .getStatusBarContentInsetsForRotation(ROTATION_UPSIDE_DOWN)
557 
558         return listOf(left, top, right, bottom)
559     }
560 
561     private fun setNewLayoutRects() {
562         val rects = getLayoutRects()
563 
564         synchronized(lock) {
565             nextViewState = nextViewState.copy(
566                     seascapeRect = rects[0],
567                     portraitRect = rects[1],
568                     landscapeRect = rects[2],
569                     upsideDownRect = rects[3]
570             )
571         }
572     }
573 }
574 
dlognull575 private fun dlog(s: String) {
576     if (DEBUG) {
577         Log.d(TAG, s)
578     }
579 }
580 
vlognull581 private fun vlog(s: String) {
582     if (DEBUG_VERBOSE) {
583         Log.d(TAG, s)
584     }
585 }
586 
587 const val TOP_LEFT = 0
588 const val TOP_RIGHT = 1
589 const val BOTTOM_RIGHT = 2
590 const val BOTTOM_LEFT = 3
591 private const val DURATION = 160L
592 private const val TAG = "PrivacyDotViewController"
593 private const val DEBUG = false
594 private const val DEBUG_VERBOSE = false
595 
toGravitynull596 private fun Int.toGravity(): Int {
597     return when (this) {
598         TOP_LEFT -> Gravity.TOP or Gravity.LEFT
599         TOP_RIGHT -> Gravity.TOP or Gravity.RIGHT
600         BOTTOM_LEFT -> Gravity.BOTTOM or Gravity.LEFT
601         BOTTOM_RIGHT -> Gravity.BOTTOM or Gravity.RIGHT
602         else -> throw IllegalArgumentException("Not a corner")
603     }
604 }
605 
Intnull606 private fun Int.innerGravity(): Int {
607     return when (this) {
608         TOP_LEFT -> Gravity.CENTER_VERTICAL or Gravity.RIGHT
609         TOP_RIGHT -> Gravity.CENTER_VERTICAL or Gravity.LEFT
610         BOTTOM_LEFT -> Gravity.CENTER_VERTICAL or Gravity.RIGHT
611         BOTTOM_RIGHT -> Gravity.CENTER_VERTICAL or Gravity.LEFT
612         else -> throw IllegalArgumentException("Not a corner")
613     }
614 }
615 
616 private data class ViewState(
617     val viewInitialized: Boolean = false,
618 
619     val systemPrivacyEventIsActive: Boolean = false,
620     val shadeExpanded: Boolean = false,
621     val qsExpanded: Boolean = false,
622 
623     val portraitRect: Rect? = null,
624     val landscapeRect: Rect? = null,
625     val upsideDownRect: Rect? = null,
626     val seascapeRect: Rect? = null,
627     val layoutRtl: Boolean = false,
628 
629     val rotation: Int = 0,
630     val height: Int = 0,
631     val cornerIndex: Int = -1,
632     val designatedCorner: View? = null,
633 
634     val contentDescription: String? = null
635 ) {
shouldShowDotnull636     fun shouldShowDot(): Boolean {
637         return systemPrivacyEventIsActive && !shadeExpanded && !qsExpanded
638     }
639 
needsLayoutnull640     fun needsLayout(other: ViewState): Boolean {
641         return rotation != other.rotation ||
642                 layoutRtl != other.layoutRtl ||
643                 portraitRect != other.portraitRect ||
644                 landscapeRect != other.landscapeRect ||
645                 upsideDownRect != other.upsideDownRect ||
646                 seascapeRect != other.seascapeRect
647     }
648 
contentRectForRotationnull649     fun contentRectForRotation(@Rotation rot: Int): Rect {
650         return when (rot) {
651             ROTATION_NONE -> portraitRect!!
652             ROTATION_LANDSCAPE -> landscapeRect!!
653             ROTATION_UPSIDE_DOWN -> upsideDownRect!!
654             ROTATION_SEASCAPE -> seascapeRect!!
655             else -> throw IllegalArgumentException("not a rotation ($rot)")
656         }
657     }
658 }
659