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