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