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