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