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