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