From 8f29f62631ae332bff0e20dc2f7fff14081c45cd Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Tue, 4 May 2021 20:32:44 -0400 Subject: [PATCH] Privacy chip (3/many) - Don't show the privacy dot when the shade is expanded. - Rework the privacy dot view controller to use a view state + resolver so we don't miss states - Add a way for ThreadFactory to create Executors given a Looper Test: atest SystemUITests Bug: 177323724 Bug: 186039244 Change-Id: I272eb3135fe6a53dfb5e7aac8a1b165bc4ad3d43 --- .../android/systemui/ScreenDecorations.java | 11 +- .../events/PrivacyDotViewController.kt | 325 ++++++++++++------ .../events/SystemStatusAnimationScheduler.kt | 12 +- .../util/concurrency/ThreadFactory.java | 9 +- .../util/concurrency/ThreadFactoryImpl.java | 5 + .../systemui/ScreenDecorationsTest.java | 9 +- .../util/concurrency/FakeThreadFactory.java | 7 + 7 files changed, 253 insertions(+), 125 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index d07723e475fbf..be50eb1934ad4 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index eb3a17f4ccce4..2481ed4828728 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -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 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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt index b6f041685dd14..655ed41327260 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt @@ -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 = 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 } } diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactory.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactory.java index 0352fb51bc214..2270f96310948 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactory.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactory.java @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactoryImpl.java index ca8d836076343..2d9f2b424aae6 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ThreadFactoryImpl.java @@ -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); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 325275047ebb0..b9ce20373755e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -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(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeThreadFactory.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeThreadFactory.java index 8c92482220145..ce71ac880b078 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeThreadFactory.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeThreadFactory.java @@ -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; + } }