Loading packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +8 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; Loading @@ -223,6 +226,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { mTunerService = tunerService; mUserTracker = userTracker; mDotViewController = dotViewController; mThreadFactory = threadFactory; } @Override Loading @@ -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 Loading Loading @@ -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) { Loading packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +209 −110 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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) { 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() 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 } } rotation = rot needsRotationUpdate = true @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 Loading @@ -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() } Loading @@ -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 Loading @@ -177,25 +231,19 @@ 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() private fun updateDesignatedCorner(newCorner: View?, shouldShowDot: Boolean) { if (shouldShowDot) { newCorner?.apply { clearAnimation() visibility = View.VISIBLE alpha = 0f animate() .alpha(1.0f) .setDuration(300) .start() } } } @UiThread private fun setCornerVisibilities(vis: Int) { Loading @@ -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 Loading @@ -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") } } Loading @@ -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) } } /** Loading @@ -257,8 +311,6 @@ class PrivacyDotViewController @Inject constructor( fun setStatusBarHeights(portrait: Int, landscape: Int) { sbHeightPortrait = portrait sbHeightLandscape = landscape hasMultipleHeights = portrait != landscape } /** Loading @@ -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 dlog("setStatusBarMargins l=$left r=$right") synchronized(lock) { nextViewState = nextViewState.copy(marginLeft = left, marginRight = right) } } /** * We won't show the dot when quick settings is showing */ private fun setStatusBarExpanded(expanded: Boolean) { synchronized(lock) { nextViewState = nextViewState.copy(hideDotForQuickSettings = expanded) } } needsMarginUpdate = true private fun scheduleUpdate() { dlog("scheduleUpdate: ") // 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) cancelRunnable?.run() cancelRunnable = uiExecutor?.executeDelayed({ processNextViewState() }, 100) } @UiThread private fun processNextViewState() { dlog("processNextViewState: ") val newState: ViewState synchronized(lock) { newState = nextViewState.copy() } private fun doUpdates(rot: Boolean, height: Boolean, width: Boolean) { dlog("doUpdates: ") var newDesignatedCorner: View? = null resolveState(newState) } if (rot) { needsRotationUpdate = false updateRotations() newDesignatedCorner = selectDesignatedCorner() @UiThread private fun resolveState(state: ViewState) { dlog("resolveState $state") if (state == currentViewState) { dlog("resolveState: skipping") return } if (height) { needsHeightUpdate = false updateHeights(rotation) if (state.rotation != currentViewState.rotation) { // A rotation has started, hide the views to avoid flicker updateRotations(state.rotation) } if (width) { needsMarginUpdate = false updateCornerSizes() if (state.height != currentViewState.height) { updateHeights(state.rotation) } if (newDesignatedCorner != null && newDesignatedCorner != designatedCorner) { updateDesignatedCorner(newDesignatedCorner) 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()) } 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 if (state.needsLayout(currentViewState)) { views.forEach { it.requestLayout() } } 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 } 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) } }) return alpha } override fun onHidePersistentDot(): Animator? { if (designatedCorner == null) { currentViewState = state } private val systemStatusAnimationCallback: SystemStatusAnimationCallback = object : SystemStatusAnimationCallback { override fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? { synchronized(lock) { nextViewState = nextViewState.copy(systemPrivacyEventIsActive = true) } return null } 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 } override fun onHidePersistentDot(): Animator? { synchronized(lock) { nextViewState = nextViewState.copy(systemPrivacyEventIsActive = false) } }) alpha.start() return null } } private fun View?.cornerIndex(): Int { if (this != null) { return cornerForView(this) } return -1 } } private fun dlog(s: String) { Loading Loading @@ -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 } } packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt +5 −7 Original line number Diff line number Diff line Loading @@ -107,7 +107,7 @@ class SystemStatusAnimationScheduler @Inject constructor( scheduleEvent(event) } else if (event.forceVisible) { hasPersistentDot = true notifyTransitionToPersistentDot(showAnimation = false) notifyTransitionToPersistentDot() } } else { if (DEBUG) { Loading Loading @@ -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) } Loading @@ -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() Loading Loading @@ -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 } } Loading packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactory.java +8 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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); } packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactoryImpl.java +5 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.util.concurrency; import android.os.HandlerThread; import android.os.Looper; import java.util.concurrent.Executor; Loading @@ -35,4 +36,8 @@ class ThreadFactoryImpl implements ThreadFactory { handlerThread.start(); return new ExecutorImpl(handlerThread.getLooper()); } public DelayableExecutor buildDelayableExecutorOnLooper(Looper looper) { return new ExecutorImpl(looper); } } Loading
packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +8 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; Loading @@ -223,6 +226,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { mTunerService = tunerService; mUserTracker = userTracker; mDotViewController = dotViewController; mThreadFactory = threadFactory; } @Override Loading @@ -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 Loading Loading @@ -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) { Loading
packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +209 −110 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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) { 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() 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 } } rotation = rot needsRotationUpdate = true @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 Loading @@ -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() } Loading @@ -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 Loading @@ -177,25 +231,19 @@ 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() private fun updateDesignatedCorner(newCorner: View?, shouldShowDot: Boolean) { if (shouldShowDot) { newCorner?.apply { clearAnimation() visibility = View.VISIBLE alpha = 0f animate() .alpha(1.0f) .setDuration(300) .start() } } } @UiThread private fun setCornerVisibilities(vis: Int) { Loading @@ -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 Loading @@ -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") } } Loading @@ -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) } } /** Loading @@ -257,8 +311,6 @@ class PrivacyDotViewController @Inject constructor( fun setStatusBarHeights(portrait: Int, landscape: Int) { sbHeightPortrait = portrait sbHeightLandscape = landscape hasMultipleHeights = portrait != landscape } /** Loading @@ -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 dlog("setStatusBarMargins l=$left r=$right") synchronized(lock) { nextViewState = nextViewState.copy(marginLeft = left, marginRight = right) } } /** * We won't show the dot when quick settings is showing */ private fun setStatusBarExpanded(expanded: Boolean) { synchronized(lock) { nextViewState = nextViewState.copy(hideDotForQuickSettings = expanded) } } needsMarginUpdate = true private fun scheduleUpdate() { dlog("scheduleUpdate: ") // 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) cancelRunnable?.run() cancelRunnable = uiExecutor?.executeDelayed({ processNextViewState() }, 100) } @UiThread private fun processNextViewState() { dlog("processNextViewState: ") val newState: ViewState synchronized(lock) { newState = nextViewState.copy() } private fun doUpdates(rot: Boolean, height: Boolean, width: Boolean) { dlog("doUpdates: ") var newDesignatedCorner: View? = null resolveState(newState) } if (rot) { needsRotationUpdate = false updateRotations() newDesignatedCorner = selectDesignatedCorner() @UiThread private fun resolveState(state: ViewState) { dlog("resolveState $state") if (state == currentViewState) { dlog("resolveState: skipping") return } if (height) { needsHeightUpdate = false updateHeights(rotation) if (state.rotation != currentViewState.rotation) { // A rotation has started, hide the views to avoid flicker updateRotations(state.rotation) } if (width) { needsMarginUpdate = false updateCornerSizes() if (state.height != currentViewState.height) { updateHeights(state.rotation) } if (newDesignatedCorner != null && newDesignatedCorner != designatedCorner) { updateDesignatedCorner(newDesignatedCorner) 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()) } 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 if (state.needsLayout(currentViewState)) { views.forEach { it.requestLayout() } } 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 } 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) } }) return alpha } override fun onHidePersistentDot(): Animator? { if (designatedCorner == null) { currentViewState = state } private val systemStatusAnimationCallback: SystemStatusAnimationCallback = object : SystemStatusAnimationCallback { override fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? { synchronized(lock) { nextViewState = nextViewState.copy(systemPrivacyEventIsActive = true) } return null } 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 } override fun onHidePersistentDot(): Animator? { synchronized(lock) { nextViewState = nextViewState.copy(systemPrivacyEventIsActive = false) } }) alpha.start() return null } } private fun View?.cornerIndex(): Int { if (this != null) { return cornerForView(this) } return -1 } } private fun dlog(s: String) { Loading Loading @@ -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 } }
packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt +5 −7 Original line number Diff line number Diff line Loading @@ -107,7 +107,7 @@ class SystemStatusAnimationScheduler @Inject constructor( scheduleEvent(event) } else if (event.forceVisible) { hasPersistentDot = true notifyTransitionToPersistentDot(showAnimation = false) notifyTransitionToPersistentDot() } } else { if (DEBUG) { Loading Loading @@ -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) } Loading @@ -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() Loading Loading @@ -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 } } Loading
packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactory.java +8 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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); }
packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactoryImpl.java +5 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.util.concurrency; import android.os.HandlerThread; import android.os.Looper; import java.util.concurrent.Executor; Loading @@ -35,4 +36,8 @@ class ThreadFactoryImpl implements ThreadFactory { handlerThread.start(); return new ExecutorImpl(handlerThread.getLooper()); } public DelayableExecutor buildDelayableExecutorOnLooper(Looper looper) { return new ExecutorImpl(looper); } }