Merge "Privacy chip (3/many)" into sc-dev

This commit is contained in:
Evan Laird
2021-05-07 18:15:11 +00:00
committed by Android (Google) Code Review
7 changed files with 253 additions and 125 deletions

View File

@@ -91,6 +91,7 @@ import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.concurrency.ThreadFactory;
import com.android.systemui.util.settings.SecureSettings;
import java.util.ArrayList;
@@ -128,6 +129,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {
private CameraAvailabilityListener mCameraListener;
private final UserTracker mUserTracker;
private final PrivacyDotViewController mDotViewController;
private final ThreadFactory mThreadFactory;
//TODO: These are piecemeal being updated to Points for now to support non-square rounded
// corners. for now it is only supposed when reading the intrinsic size from the drawables with
@@ -215,7 +217,8 @@ public class ScreenDecorations extends SystemUI implements Tunable {
BroadcastDispatcher broadcastDispatcher,
TunerService tunerService,
UserTracker userTracker,
PrivacyDotViewController dotViewController) {
PrivacyDotViewController dotViewController,
ThreadFactory threadFactory) {
super(context);
mMainHandler = handler;
mSecureSettings = secureSettings;
@@ -223,6 +226,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {
mTunerService = tunerService;
mUserTracker = userTracker;
mDotViewController = dotViewController;
mThreadFactory = threadFactory;
}
@Override
@@ -233,7 +237,8 @@ public class ScreenDecorations extends SystemUI implements Tunable {
}
mHandler = startHandlerThread();
mHandler.post(this::startOnScreenDecorationsThread);
mDotViewController.setUiExecutor(mHandler::post);
mDotViewController.setUiExecutor(
mThreadFactory.buildDelayableExecutorOnLooper(mHandler.getLooper()));
}
@VisibleForTesting
@@ -643,7 +648,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {
int newRotation = mContext.getDisplay().getRotation();
if (mRotation != newRotation) {
mDotViewController.updateRotation(newRotation);
mDotViewController.setNewRotation(newRotation);
}
if (mPendingRotationChange) {

View File

@@ -17,20 +17,25 @@
package com.android.systemui.statusbar.events
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.annotation.UiThread
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import com.android.internal.annotations.GuardedBy
import com.android.systemui.animation.Interpolators
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.StatusBarLocationPublisher
import com.android.systemui.statusbar.phone.StatusBarMarginUpdatedListener
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
import java.lang.IllegalStateException
import java.util.concurrent.Executor
@@ -54,31 +59,33 @@ import javax.inject.Inject
@SysUISingleton
class PrivacyDotViewController @Inject constructor(
@Main private val mainExecutor: Executor,
private val stateController: StatusBarStateController,
private val locationPublisher: StatusBarLocationPublisher,
private val animationScheduler: SystemStatusAnimationScheduler
) {
private var rotation = 0
private var leftSize = 0
private var rightSize = 0
private var sbHeightPortrait = 0
private var sbHeightLandscape = 0
private var hasMultipleHeights = false
private var needsHeightUpdate = false
private var needsRotationUpdate = false
private var needsMarginUpdate = false
private lateinit var tl: View
private lateinit var tr: View
private lateinit var bl: View
private lateinit var br: View
// Track which corner is active (based on orientation + RTL)
private var designatedCorner: View? = null
// Only can be modified on @UiThread
private var currentViewState: ViewState = ViewState()
@GuardedBy("lock")
private var nextViewState: ViewState = currentViewState.copy()
set(value) {
field = value
scheduleUpdate()
}
private val lock = Object()
private var cancelRunnable: Runnable? = null
// Privacy dots are created in ScreenDecoration's UiThread, which is not the main thread
private var uiExecutor: Executor? = null
private var uiExecutor: DelayableExecutor? = null
private var e: DelayableExecutor? = null
private val views: Sequence<View>
get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl)
@@ -89,29 +96,78 @@ class PrivacyDotViewController @Inject constructor(
setStatusBarMargins(marginLeft, marginRight)
}
})
stateController.addCallback(object : StatusBarStateController.StateListener {
override fun onExpandedChanged(isExpanded: Boolean) {
setStatusBarExpanded(isExpanded)
}
})
}
fun setUiExecutor(e: Executor) {
fun setUiExecutor(e: DelayableExecutor) {
uiExecutor = e
}
@UiThread
fun updateRotation(rot: Int) {
dlog("updateRotation: ")
if (rot == rotation) {
return
fun setNewRotation(rot: Int) {
dlog("updateRotation: $rot")
synchronized(lock) {
if (rot == nextViewState.rotation) {
return
}
}
// A rotation has started, hide the views to avoid flicker
// If we rotated, hide all dotes until the next state resolves
setCornerVisibilities(View.INVISIBLE)
if (hasMultipleHeights && (rotation % 2) != (rot % 2)) {
// we've changed from vertical to horizontal; update status bar height
needsHeightUpdate = true
}
val newCorner = selectDesignatedCorner(rot)
val index = newCorner.cornerIndex()
rotation = rot
needsRotationUpdate = true
val h = when (rot) {
ROTATION_NONE, ROTATION_UPSIDE_DOWN -> sbHeightPortrait
ROTATION_LANDSCAPE, ROTATION_SEASCAPE -> sbHeightLandscape
else -> 0
}
synchronized(lock) {
nextViewState = nextViewState.copy(
rotation = rot,
height = h,
designatedCorner = newCorner,
cornerIndex = index)
}
}
@UiThread
private fun hideDotView(dot: View, animate: Boolean) {
dot.clearAnimation()
if (animate) {
dot.animate()
.setDuration(DURATION)
.setInterpolator(Interpolators.ALPHA_OUT)
.alpha(0f)
.withEndAction { dot.visibility = View.INVISIBLE }
.start()
} else {
dot.visibility = View.INVISIBLE
}
}
@UiThread
private fun showDotView(dot: View, animate: Boolean) {
dot.clearAnimation()
if (animate) {
dot.visibility = View.VISIBLE
dot.alpha = 0f
dot.animate()
.alpha(1f)
.setDuration(DURATION)
.setInterpolator(Interpolators.ALPHA_IN)
.start()
} else {
dot.visibility = View.VISIBLE
dot.alpha = 1f
}
}
@UiThread
@@ -127,14 +183,14 @@ class PrivacyDotViewController @Inject constructor(
// Update the gravity and margins of the privacy views
@UiThread
private fun updateRotations() {
private fun updateRotations(rotation: Int) {
// To keep a view in the corner, its gravity is always the description of its current corner
// Therefore, just figure out which view is in which corner. This turns out to be something
// like (myCorner - rot) mod 4, where topLeft = 0, topRight = 1, etc. and portrait = 0, and
// rotating the device counter-clockwise increments rotation by 1
views.forEach { corner ->
val rotatedCorner = rotatedCorner(cornerForView(corner))
val rotatedCorner = rotatedCorner(cornerForView(corner), rotation)
(corner.layoutParams as FrameLayout.LayoutParams).apply {
gravity = rotatedCorner.toGravity()
}
@@ -147,26 +203,24 @@ class PrivacyDotViewController @Inject constructor(
}
@UiThread
private fun updateCornerSizes() {
private fun updateCornerSizes(l: Int, r: Int, rotation: Int) {
views.forEach { corner ->
val rotatedCorner = rotatedCorner(cornerForView(corner))
val w = widthForCorner(rotatedCorner)
Log.d(TAG, "updateCornerSizes: setting (${cornerForView(corner)}) to $w")
val rotatedCorner = rotatedCorner(cornerForView(corner), rotation)
val w = widthForCorner(rotatedCorner, l, r)
(corner.layoutParams as FrameLayout.LayoutParams).width = w
corner.requestLayout()
}
}
// Designated view will be the one at statusbar's view.END
@UiThread
private fun selectDesignatedCorner(): View? {
private fun selectDesignatedCorner(r: Int): View? {
if (!this::tl.isInitialized) {
return null
}
val isRtl = tl.isLayoutRtl
return when (rotation) {
return when (r) {
0 -> if (isRtl) tl else tr
1 -> if (isRtl) tr else br
2 -> if (isRtl) br else bl
@@ -177,23 +231,17 @@ class PrivacyDotViewController @Inject constructor(
// Track the current designated corner and maybe animate to a new rotation
@UiThread
private fun updateDesignatedCorner(newCorner: View) {
designatedCorner = newCorner
if (animationScheduler.hasPersistentDot) {
fadeInDot()
}
}
@UiThread
private fun fadeInDot() {
designatedCorner?.let { dot ->
dot.visibility = View.VISIBLE
dot.alpha = 0f
dot.animate()
.alpha(1.0f)
.setDuration(300)
.start()
private fun updateDesignatedCorner(newCorner: View?, shouldShowDot: Boolean) {
if (shouldShowDot) {
newCorner?.apply {
clearAnimation()
visibility = View.VISIBLE
alpha = 0f
animate()
.alpha(1.0f)
.setDuration(300)
.start()
}
}
}
@@ -214,7 +262,7 @@ class PrivacyDotViewController @Inject constructor(
}
}
private fun rotatedCorner(corner: Int): Int {
private fun rotatedCorner(corner: Int, rotation: Int): Int {
var modded = corner - rotation
if (modded < 0) {
modded += 4
@@ -223,10 +271,10 @@ class PrivacyDotViewController @Inject constructor(
return modded
}
private fun widthForCorner(corner: Int): Int {
private fun widthForCorner(corner: Int, left: Int, right: Int): Int {
return when (corner) {
TOP_LEFT, BOTTOM_LEFT -> leftSize
TOP_RIGHT, BOTTOM_RIGHT -> rightSize
TOP_LEFT, BOTTOM_LEFT -> left
TOP_RIGHT, BOTTOM_RIGHT -> right
else -> throw IllegalArgumentException("Unknown corner")
}
}
@@ -244,10 +292,16 @@ class PrivacyDotViewController @Inject constructor(
bl = bottomLeft
br = bottomRight
designatedCorner = selectDesignatedCorner()
val dc = selectDesignatedCorner(0)
val index = dc.cornerIndex()
mainExecutor.execute {
animationScheduler.addCallback(systemStatusAnimationCallback)
}
synchronized(lock) {
nextViewState = nextViewState.copy(designatedCorner = dc, cornerIndex = index)
}
}
/**
@@ -257,8 +311,6 @@ class PrivacyDotViewController @Inject constructor(
fun setStatusBarHeights(portrait: Int, landscape: Int) {
sbHeightPortrait = portrait
sbHeightLandscape = landscape
hasMultipleHeights = portrait != landscape
}
/**
@@ -268,85 +320,109 @@ class PrivacyDotViewController @Inject constructor(
* @param right space between the status bar contents and the right side of the screen
*/
private fun setStatusBarMargins(left: Int, right: Int) {
leftSize = left
rightSize = right
needsMarginUpdate = true
// Margins come after PhoneStatusBarView does a layout pass, and so will always happen
// after rotation changes. It is safe to execute the updates from here
uiExecutor?.execute {
doUpdates(needsRotationUpdate, needsHeightUpdate, needsMarginUpdate)
dlog("setStatusBarMargins l=$left r=$right")
synchronized(lock) {
nextViewState = nextViewState.copy(marginLeft = left, marginRight = right)
}
}
private fun doUpdates(rot: Boolean, height: Boolean, width: Boolean) {
dlog("doUpdates: ")
var newDesignatedCorner: View? = null
/**
* We won't show the dot when quick settings is showing
*/
private fun setStatusBarExpanded(expanded: Boolean) {
synchronized(lock) {
nextViewState = nextViewState.copy(hideDotForQuickSettings = expanded)
}
}
if (rot) {
needsRotationUpdate = false
updateRotations()
newDesignatedCorner = selectDesignatedCorner()
private fun scheduleUpdate() {
dlog("scheduleUpdate: ")
cancelRunnable?.run()
cancelRunnable = uiExecutor?.executeDelayed({
processNextViewState()
}, 100)
}
@UiThread
private fun processNextViewState() {
dlog("processNextViewState: ")
val newState: ViewState
synchronized(lock) {
newState = nextViewState.copy()
}
if (height) {
needsHeightUpdate = false
updateHeights(rotation)
resolveState(newState)
}
@UiThread
private fun resolveState(state: ViewState) {
dlog("resolveState $state")
if (state == currentViewState) {
dlog("resolveState: skipping")
return
}
if (width) {
needsMarginUpdate = false
updateCornerSizes()
if (state.rotation != currentViewState.rotation) {
// A rotation has started, hide the views to avoid flicker
updateRotations(state.rotation)
}
if (newDesignatedCorner != null && newDesignatedCorner != designatedCorner) {
updateDesignatedCorner(newDesignatedCorner)
if (state.height != currentViewState.height) {
updateHeights(state.rotation)
}
if (state.marginLeft != currentViewState.marginLeft ||
state.marginRight != currentViewState.marginRight) {
updateCornerSizes(state.marginLeft, state.marginRight, state.rotation)
}
if (state.designatedCorner != currentViewState.designatedCorner) {
updateDesignatedCorner(state.designatedCorner, state.shouldShowDot())
}
if (state.needsLayout(currentViewState)) {
views.forEach { it.requestLayout() }
}
val shouldShow = state.shouldShowDot()
if (shouldShow != currentViewState.shouldShowDot()) {
if (shouldShow && state.designatedCorner != null) {
showDotView(state.designatedCorner, true)
} else if (!shouldShow && state.designatedCorner != null) {
hideDotView(state.designatedCorner, true)
}
}
currentViewState = state
}
private val systemStatusAnimationCallback: SystemStatusAnimationCallback =
object : SystemStatusAnimationCallback {
override fun onSystemStatusAnimationTransitionToPersistentDot(
showAnimation: Boolean
): Animator? {
if (designatedCorner == null) {
return null
} else if (!showAnimation) {
uiExecutor?.execute { fadeInDot() }
return null
override fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? {
synchronized(lock) {
nextViewState = nextViewState.copy(systemPrivacyEventIsActive = true)
}
val alpha = ObjectAnimator.ofFloat(
designatedCorner, "alpha", 0f, 1f)
alpha.duration = DURATION
alpha.interpolator = Interpolators.ALPHA_OUT
alpha.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animator: Animator) {
uiExecutor?.execute { designatedCorner?.visibility = View.VISIBLE }
}
})
return alpha
return null
}
override fun onHidePersistentDot(): Animator? {
if (designatedCorner == null) {
return null
synchronized(lock) {
nextViewState = nextViewState.copy(systemPrivacyEventIsActive = false)
}
val alpha = ObjectAnimator.ofFloat(
designatedCorner, "alpha", 1f, 0f)
alpha.duration = DURATION
alpha.interpolator = Interpolators.ALPHA_OUT
alpha.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animator: Animator) {
uiExecutor?.execute { designatedCorner?.visibility = View.INVISIBLE }
}
})
alpha.start()
return null
}
}
private fun View?.cornerIndex(): Int {
if (this != null) {
return cornerForView(this)
}
return -1
}
}
private fun dlog(s: String) {
@@ -382,3 +458,26 @@ private fun Int.innerGravity(): Int {
else -> throw IllegalArgumentException("Not a corner")
}
}
private data class ViewState(
// don't @ me with names
val systemPrivacyEventIsActive: Boolean = false,
val hideDotForQuickSettings: Boolean = false,
val statusBarExpanded: Boolean = false,
val rotation: Int = 0,
val height: Int = 0,
val marginLeft: Int = 0,
val marginRight: Int = 0,
val cornerIndex: Int = -1,
val designatedCorner: View? = null
) {
fun shouldShowDot(): Boolean {
return systemPrivacyEventIsActive && !hideDotForQuickSettings
}
fun needsLayout(other: ViewState): Boolean {
return rotation != other.rotation ||
marginRight != other.marginRight ||
height != other.height
}
}

View File

@@ -107,7 +107,7 @@ class SystemStatusAnimationScheduler @Inject constructor(
scheduleEvent(event)
} else if (event.forceVisible) {
hasPersistentDot = true
notifyTransitionToPersistentDot(showAnimation = false)
notifyTransitionToPersistentDot()
}
} else {
if (DEBUG) {
@@ -202,7 +202,7 @@ class SystemStatusAnimationScheduler @Inject constructor(
aSet2.play(chipAnimator).before(systemAnimator)
if (hasPersistentDot) {
val dotAnim = notifyTransitionToPersistentDot(showAnimation = true)
val dotAnim = notifyTransitionToPersistentDot()
if (dotAnim != null) aSet2.playTogether(systemAnimator, dotAnim)
}
@@ -214,9 +214,9 @@ class SystemStatusAnimationScheduler @Inject constructor(
}, DELAY)
}
private fun notifyTransitionToPersistentDot(showAnimation: Boolean): Animator? {
private fun notifyTransitionToPersistentDot(): Animator? {
val anims: List<Animator> = listeners.mapNotNull {
it.onSystemStatusAnimationTransitionToPersistentDot(showAnimation)
it.onSystemStatusAnimationTransitionToPersistentDot()
}
if (anims.isNotEmpty()) {
val aSet = AnimatorSet()
@@ -326,9 +326,7 @@ interface SystemStatusAnimationCallback {
@JvmDefault fun onSystemChromeAnimationEnd() {}
// Best method name, change my mind
@JvmDefault fun onSystemStatusAnimationTransitionToPersistentDot(
showAnimation: Boolean
): Animator? { return null }
@JvmDefault fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? { return null }
@JvmDefault fun onHidePersistentDot(): Animator? { return null }
}

View File

@@ -16,13 +16,15 @@
package com.android.systemui.util.concurrency;
import android.os.Looper;
import java.util.concurrent.Executor;
/**
* Factory for building Executors running on a unique named thread.
*
* Use this when our generally available @Main, @Background, @UiBackground, @LongRunning, or
* similar global qualifiers don't quite cut it. Note that the methods here create entirely new
* similar global qualifiers don't quite cut it. Note that the methods here can create entirely new
* threads; there are no singletons here. Use responsibly.
*/
public interface ThreadFactory {
@@ -41,4 +43,9 @@ public interface ThreadFactory {
* implementation. Assume this is the case and use responsibly.
**/
DelayableExecutor buildDelayableExecutorOnNewThread(String threadName);
/**
* Return an {@link DelayableExecutor} running the given Looper
**/
DelayableExecutor buildDelayableExecutorOnLooper(Looper looper);
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.util.concurrency;
import android.os.HandlerThread;
import android.os.Looper;
import java.util.concurrent.Executor;
@@ -35,4 +36,8 @@ class ThreadFactoryImpl implements ThreadFactory {
handlerThread.start();
return new ExecutorImpl(handlerThread.getLooper());
}
public DelayableExecutor buildDelayableExecutorOnLooper(Looper looper) {
return new ExecutorImpl(looper);
}
}

View File

@@ -65,8 +65,12 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.concurrency.FakeThreadFactory;
import com.android.systemui.util.concurrency.ThreadFactory;
import com.android.systemui.util.settings.FakeSettings;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
@@ -89,6 +93,7 @@ public class ScreenDecorationsTest extends SysuiTestCase {
private DisplayManager mDisplayManager;
private SecureSettings mSecureSettings;
private Handler mMainHandler;
private ThreadFactory mThreadFactory;
@Mock
private TunerService mTunerService;
@Mock
@@ -105,6 +110,7 @@ public class ScreenDecorationsTest extends SysuiTestCase {
mTestableLooper = TestableLooper.get(this);
mMainHandler = new Handler(mTestableLooper.getLooper());
mSecureSettings = new FakeSettings();
mThreadFactory = new FakeThreadFactory(new FakeExecutor(new FakeSystemClock()));
mWindowManager = mock(WindowManager.class);
WindowMetrics metrics = mContext.getSystemService(WindowManager.class)
@@ -119,7 +125,8 @@ public class ScreenDecorationsTest extends SysuiTestCase {
mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
mScreenDecorations = spy(new ScreenDecorations(mContext, mMainHandler, mSecureSettings,
mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController) {
mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController,
mThreadFactory) {
@Override
public void start() {
super.start();

View File

@@ -16,6 +16,8 @@
package com.android.systemui.util.concurrency;
import android.os.Looper;
import java.util.concurrent.Executor;
/**
@@ -37,4 +39,9 @@ public class FakeThreadFactory implements ThreadFactory {
public DelayableExecutor buildDelayableExecutorOnNewThread(String threadName) {
return mFakeExecutor;
}
@Override
public DelayableExecutor buildDelayableExecutorOnLooper(Looper looper) {
return mFakeExecutor;
}
}