diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 4d89caf1591ea..442313d763ecc 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.phone.KeyguardLiftController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarComponent; import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.util.concurrency.ConcurrencyModule; import com.android.systemui.util.sensors.AsyncSensorManager; import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClockImpl; @@ -53,6 +54,7 @@ import dagger.Provides; * implementation. */ @Module(includes = {AssistModule.class, + ConcurrencyModule.class, PeopleHubModule.class}, subcomponents = {StatusBarComponent.class}) public abstract class SystemUIModule { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/Background.java b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/Background.java new file mode 100644 index 0000000000000..141c9019b3e47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/Background.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dagger.qualifiers; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface Background { +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/Main.java b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/Main.java new file mode 100644 index 0000000000000..7b097740ff112 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/Main.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dagger.qualifiers; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface Main { +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 0a9100f6c7d51..f30c181b3c991 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -25,7 +25,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.Build; -import android.os.Handler; import android.provider.Settings; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; @@ -34,8 +33,8 @@ import android.util.ArraySet; import android.widget.Button; import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.BgHandler; -import com.android.systemui.dagger.qualifiers.MainHandler; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.QSTileHost; @@ -47,6 +46,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -55,8 +55,8 @@ public class TileQueryHelper { private final ArrayList mTiles = new ArrayList<>(); private final ArraySet mSpecs = new ArraySet<>(); - private final Handler mBgHandler; - private final Handler mMainHandler; + private final Executor mMainExecutor; + private final Executor mBgExecutor; private final Context mContext; private TileStateListener mListener; @@ -64,10 +64,10 @@ public class TileQueryHelper { @Inject public TileQueryHelper(Context context, - @MainHandler Handler mainHandler, @BgHandler Handler bgHandler) { + @Main Executor mainExecutor, @Background Executor bgExecutor) { mContext = context; - mMainHandler = mainHandler; - mBgHandler = bgHandler; + mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; } public void setListener(TileStateListener listener) { @@ -126,7 +126,7 @@ public class TileQueryHelper { tilesToAdd.add(tile); } - mBgHandler.post(() -> { + mBgExecutor.execute(() -> { for (QSTile tile : tilesToAdd) { final QSTile.State state = tile.getState().copy(); // Ignore the current state and get the generic label instead. @@ -139,7 +139,7 @@ public class TileQueryHelper { } private void addPackageTiles(final QSTileHost host) { - mBgHandler.post(() -> { + mBgExecutor.execute(() -> { Collection params = host.getTiles(); PackageManager pm = mContext.getPackageManager(); List services = pm.queryIntentServicesAsUser( @@ -185,7 +185,7 @@ public class TileQueryHelper { private void notifyTilesChanged(final boolean finished) { final ArrayList tilesToReturn = new ArrayList<>(mTiles); - mMainHandler.post(() -> { + mMainExecutor.execute(() -> { if (mListener != null) { mListener.onTilesChanged(tilesToReturn); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java index 341c49a871568..2005d794c9d38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java @@ -17,14 +17,13 @@ package com.android.systemui.statusbar; import android.annotation.NonNull; -import android.os.Handler; -import android.os.HandlerExecutor; import android.provider.DeviceConfig; import android.util.ArrayMap; -import com.android.systemui.dagger.qualifiers.BgHandler; +import com.android.systemui.dagger.qualifiers.Background; import java.util.Map; +import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Singleton; @@ -49,10 +48,10 @@ public class FeatureFlags { private final Map mCachedDeviceConfigFlags = new ArrayMap<>(); @Inject - public FeatureFlags(@BgHandler Handler bgHandler) { + public FeatureFlags(@Background Executor executor) { DeviceConfig.addOnPropertiesChangedListener( "systemui", - new HandlerExecutor(bgHandler), + executor, this::onPropertiesChanged); } diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java new file mode 100644 index 0000000000000..24f49ff998791 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.BgHandler; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dagger.qualifiers.MainHandler; + +import java.util.concurrent.Executor; + +import dagger.Module; +import dagger.Provides; + +/** + * Dagger Module for classes found within the concurrent package. + */ +@Module +public abstract class ConcurrencyModule { + /** + * Provide a Background-Thread Executor by default. + */ + @Provides + public static Executor provideExecutor(@BgHandler Handler handler) { + return new ExecutorImpl(handler); + } + + /** + * Provide a Background-Thread Executor. + */ + @Provides + @Background + public static Executor provideBackgroundExecutor(@BgHandler Handler handler) { + return new ExecutorImpl(handler); + } + + /** + * Provide a Main-Thread Executor. + */ + @Provides + @Main + public static Executor provideMainExecutor(Context context) { + return context.getMainExecutor(); + } + + /** + * Provide a Background-Thread Executor by default. + */ + @Provides + public static DelayableExecutor provideDelayableExecutor(@BgHandler Handler handler) { + return new ExecutorImpl(handler); + } + + /** + * Provide a Background-Thread Executor. + */ + @Provides + @Background + public static DelayableExecutor provideBackgroundDelayableExecutor(@BgHandler Handler handler) { + return new ExecutorImpl(handler); + } + + /** + * Provide a Main-Thread Executor. + */ + @Provides + @Main + public static DelayableExecutor provideMainDelayableExecutor(@MainHandler Handler handler) { + return new ExecutorImpl(handler); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/DelayableExecutor.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/DelayableExecutor.java new file mode 100644 index 0000000000000..2d6c4a62047d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/DelayableExecutor.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * A sub-class of {@link Executor} that allows Runnables to be delayed and/or cancelled. + */ +public interface DelayableExecutor extends Executor { + /** + * Execute supplied Runnable on the Executors thread after a specified delay. + * + * See {@link android.os.Handler#postDelayed(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + default Runnable executeDelayed(Runnable r, long delayMillis) { + return executeDelayed(r, delayMillis, TimeUnit.MILLISECONDS); + } + + /** + * Execute supplied Runnable on the Executors thread after a specified delay. + * + * See {@link android.os.Handler#postDelayed(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue.. + */ + Runnable executeDelayed(Runnable r, long delay, TimeUnit unit); + + /** + * Execute supplied Runnable on the Executors thread at a specified uptime. + * + * See {@link android.os.Handler#postAtTime(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + default Runnable executeAtTime(Runnable r, long uptime) { + return executeAtTime(r, uptime, TimeUnit.MILLISECONDS); + } + + /** + * Execute supplied Runnable on the Executors thread at a specified uptime. + * + * See {@link android.os.Handler#postAtTime(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + Runnable executeAtTime(Runnable r, long uptimeMillis, TimeUnit unit); + + /** + * Remove all pending Runnables. + * + */ + void removeAll(); + +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ExecutorImpl.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ExecutorImpl.java new file mode 100644 index 0000000000000..2a99de3ac5ad8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ExecutorImpl.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.Message; + +import java.util.concurrent.TimeUnit; + +/** + * Implementations of {@link DelayableExecutor} for SystemUI. + */ +public class ExecutorImpl extends HandlerExecutor implements DelayableExecutor { + private final Handler mHandler; + + public ExecutorImpl(Handler handler) { + super(handler); + mHandler = handler; + } + + @Override + public Runnable executeDelayed(Runnable r, long delay, TimeUnit unit) { + Object token = new Object(); + Message m = mHandler.obtainMessage(0, token); + mHandler.sendMessageDelayed(m, unit.toMillis(delay)); + + return () -> mHandler.removeCallbacksAndMessages(token); + } + + @Override + public Runnable executeAtTime(Runnable r, long uptimeMillis, TimeUnit unit) { + Object token = new Object(); + Message m = mHandler.obtainMessage(0, token); + mHandler.sendMessageAtTime(m, unit.toMillis(uptimeMillis)); + + return () -> mHandler.removeCallbacksAndMessages(token); + } + + @Override + public void removeAll() { + mHandler.removeCallbacksAndMessages(null); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java index b41512c9a9351..39ce8c1209ac1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java @@ -36,13 +36,9 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.os.Handler; -import android.os.Looper; import android.provider.Settings; import android.service.quicksettings.Tile; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; import android.text.TextUtils; import android.util.ArraySet; @@ -52,6 +48,8 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSTileHost; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -69,7 +67,6 @@ import java.util.Set; @SmallTest @RunWith(AndroidTestingRunner.class) -@RunWithLooper public class TileQueryHelperTest extends SysuiTestCase { private static final String CURRENT_TILES = "wifi,dnd,nfc"; private static final String ONLY_STOCK_TILES = "wifi,dnd"; @@ -98,14 +95,13 @@ public class TileQueryHelperTest extends SysuiTestCase { private ArgumentCaptor> mCaptor; private QSTile.State mState; - private TestableLooper mBGLooper; private TileQueryHelper mTileQueryHelper; - private Handler mMainHandler; + private FakeExecutor mMainExecutor; + private FakeExecutor mBgExecutor; @Before public void setup() { MockitoAnnotations.initMocks(this); - mBGLooper = TestableLooper.get(this); mContext.setMockPackageManager(mPackageManager); mState = new QSTile.State(); @@ -123,9 +119,11 @@ public class TileQueryHelperTest extends SysuiTestCase { } ).when(mQSTileHost).createTile(anyString()); - mMainHandler = new Handler(Looper.getMainLooper()); - mTileQueryHelper = new TileQueryHelper(mContext, mMainHandler, - new Handler(mBGLooper.getLooper())); + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + mMainExecutor = new FakeExecutor(clock); + mBgExecutor = new FakeExecutor(clock); + mTileQueryHelper = new TileQueryHelper(mContext, mMainExecutor, mBgExecutor); mTileQueryHelper.setListener(mListener); } @@ -138,8 +136,7 @@ public class TileQueryHelperTest extends SysuiTestCase { public void testIsFinished_trueAfterQuerying() { mTileQueryHelper.queryTiles(mQSTileHost); - mBGLooper.processAllMessages(); - waitForIdleSync(mMainHandler); + FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); assertTrue(mTileQueryHelper.isFinished()); } @@ -148,8 +145,7 @@ public class TileQueryHelperTest extends SysuiTestCase { public void testQueryTiles_callsListenerTwice() { mTileQueryHelper.queryTiles(mQSTileHost); - mBGLooper.processAllMessages(); - waitForIdleSync(mMainHandler); + FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); verify(mListener, times(2)).onTilesChanged(any()); } @@ -163,8 +159,7 @@ public class TileQueryHelperTest extends SysuiTestCase { mTileQueryHelper.queryTiles(mQSTileHost); - mBGLooper.processAllMessages(); - waitForIdleSync(mMainHandler); + FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); assertTrue(mTileQueryHelper.isFinished()); } @@ -178,8 +173,7 @@ public class TileQueryHelperTest extends SysuiTestCase { mTileQueryHelper.queryTiles(mQSTileHost); - mBGLooper.processAllMessages(); - waitForIdleSync(mMainHandler); + FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); List specs = new ArrayList<>(); @@ -199,8 +193,7 @@ public class TileQueryHelperTest extends SysuiTestCase { mTileQueryHelper.queryTiles(mQSTileHost); - mBGLooper.processAllMessages(); - waitForIdleSync(mMainHandler); + FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); List specs = new ArrayList<>(); @@ -220,8 +213,7 @@ public class TileQueryHelperTest extends SysuiTestCase { mTileQueryHelper.queryTiles(mQSTileHost); - mBGLooper.processAllMessages(); - waitForIdleSync(mMainHandler); + FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); List specs = new ArrayList<>(); @@ -251,8 +243,7 @@ public class TileQueryHelperTest extends SysuiTestCase { ""); mTileQueryHelper.queryTiles(mQSTileHost); - mBGLooper.processAllMessages(); - waitForIdleSync(mMainHandler); + FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); List tileInfos = mCaptor.getValue(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java new file mode 100644 index 0000000000000..3ed6c5b2b11b2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.util.time.FakeSystemClock.ClockTickListener; + +import java.util.Collections; +import java.util.PriorityQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class FakeExecutor implements DelayableExecutor { + private final FakeSystemClock mClock; + private PriorityQueue mQueuedRunnables = new PriorityQueue<>(); + private boolean mIgnoreClockUpdates; + + private ClockTickListener mClockTickListener = new ClockTickListener() { + @Override + public void onUptimeMillis(long uptimeMillis) { + if (!mIgnoreClockUpdates) { + runAllReady(); + } + } + }; + + /** + * Initializes a fake executor. + * + * @param clock FakeSystemClock allowing control over delayed runnables. It is strongly + * recommended that this clock have its auto-increment setting set to false to + * prevent unexpected advancement of the time. + */ + public FakeExecutor(FakeSystemClock clock) { + mClock = clock; + mClock.addListener(mClockTickListener); + } + + /** + * Runs a single runnable if it's scheduled to run according to the internal clock. + * + * If constructed to advance the clock automatically, this will advance the clock enough to + * run the next pending item. + * + * This method does not advance the clock past the item that was run. + * + * @return Returns true if an item was run. + */ + public boolean runNextReady() { + if (!mQueuedRunnables.isEmpty() && mQueuedRunnables.peek().mWhen <= mClock.uptimeMillis()) { + mQueuedRunnables.poll().mRunnable.run(); + return true; + } + + return false; + } + + /** + * Runs all Runnables that are scheduled to run according to the internal clock. + * + * If constructed to advance the clock automatically, this will advance the clock enough to + * run all the pending items. This method does not advance the clock past items that were + * run. It is equivalent to calling {@link #runNextReady()} in a loop. + * + * @return Returns the number of items that ran. + */ + public int runAllReady() { + int num = 0; + while (runNextReady()) { + num++; + } + + return num; + } + + /** + * Advances the internal clock to the next item to run. + * + * The clock will only move forward. If the next item is set to run in the past or there is no + * next item, the clock does not change. + * + * Note that this will cause one or more items to actually run. + * + * @return The delta in uptimeMillis that the clock advanced, or 0 if the clock did not advance. + */ + public long advanceClockToNext() { + if (mQueuedRunnables.isEmpty()) { + return 0; + } + + long startTime = mClock.uptimeMillis(); + long nextTime = mQueuedRunnables.peek().mWhen; + if (nextTime <= startTime) { + return 0; + } + updateClock(nextTime); + + return nextTime - startTime; + } + + + /** + * Advances the internal clock to the last item to run. + * + * The clock will only move forward. If the last item is set to run in the past or there is no + * next item, the clock does not change. + * + * @return The delta in uptimeMillis that the clock advanced, or 0 if the clock did not advance. + */ + public long advanceClockToLast() { + if (mQueuedRunnables.isEmpty()) { + return 0; + } + + long startTime = mClock.uptimeMillis(); + long nextTime = Collections.max(mQueuedRunnables).mWhen; + if (nextTime <= startTime) { + return 0; + } + + updateClock(nextTime); + + return nextTime - startTime; + } + + /** + * Returns the number of un-executed runnables waiting to run. + */ + public int numPending() { + return mQueuedRunnables.size(); + } + + @Override + public Runnable executeDelayed(Runnable r, long delay, TimeUnit unit) { + if (delay < 0) { + delay = 0; + } + return executeAtTime(r, mClock.uptimeMillis() + unit.toMillis(delay)); + } + + @Override + public Runnable executeAtTime(Runnable r, long uptime, TimeUnit unit) { + long uptimeMillis = unit.toMillis(uptime); + + QueuedRunnable container = new QueuedRunnable(r, uptimeMillis); + + mQueuedRunnables.offer(container); + + return () -> mQueuedRunnables.remove(container); + } + + @Override + public void execute(Runnable command) { + executeDelayed(command, 0); + } + + @Override + public void removeAll() { + mQueuedRunnables.clear(); + } + + /** + * Run all Executors in a loop until they all report they have no ready work to do. + * + * Useful if you have Executors the post work to other Executors, and you simply want to + * run them all until they stop posting work. + */ + public static void exhaustExecutors(FakeExecutor ...executors) { + boolean didAnything; + do { + didAnything = false; + for (FakeExecutor executor : executors) { + didAnything = didAnything || executor.runAllReady() != 0; + } + } while (didAnything); + } + + private void updateClock(long nextTime) { + mIgnoreClockUpdates = true; + mClock.setUptimeMillis(nextTime); + mIgnoreClockUpdates = false; + } + + private static class QueuedRunnable implements Comparable { + private static AtomicInteger sCounter = new AtomicInteger(); + + Runnable mRunnable; + long mWhen; + private int mCounter; + + private QueuedRunnable(Runnable r, long when) { + mRunnable = r; + mWhen = when; + + // PrioirityQueue orders items arbitrarily when equal. We want to ensure that + // otherwise-equal elements are ordered according to their insertion order. Because this + // class only is constructed right before insertion, we use a static counter to track + // insertion order of otherwise equal elements. + mCounter = sCounter.incrementAndGet(); + } + + @Override + public int compareTo(QueuedRunnable other) { + long diff = mWhen - other.mWhen; + + if (diff == 0) { + return mCounter - other.mCounter; + } + + return diff > 0 ? 1 : -1; + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java new file mode 100644 index 0000000000000..7fd694244afab --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import kotlin.jvm.functions.Function4; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class FakeExecutorTest extends SysuiTestCase { + @Before + public void setUp() throws Exception { + } + + /** + * Test FakeExecutor that receives non-delayed items to execute. + */ + @Test + public void testNoDelay() { + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + FakeExecutor fakeExecutor = new FakeExecutor(clock); + RunnableImpl runnable = new RunnableImpl(); + + assertEquals(0, clock.uptimeMillis()); + assertEquals(0, runnable.mRunCount); + + // Execute two runnables. They should not run and should be left pending. + fakeExecutor.execute(runnable); + assertEquals(0, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(1, fakeExecutor.numPending()); + fakeExecutor.execute(runnable); + assertEquals(0, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(2, fakeExecutor.numPending()); + + // Run one pending runnable. + assertTrue(fakeExecutor.runNextReady()); + assertEquals(1, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(1, fakeExecutor.numPending()); + // Run a second pending runnable. + assertTrue(fakeExecutor.runNextReady()); + assertEquals(2, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(0, fakeExecutor.numPending()); + + // No more runnables to run. + assertFalse(fakeExecutor.runNextReady()); + + // Add two more runnables. + fakeExecutor.execute(runnable); + fakeExecutor.execute(runnable); + assertEquals(2, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(2, fakeExecutor.numPending()); + // Execute all pending runnables in batch. + assertEquals(2, fakeExecutor.runAllReady()); + assertEquals(4, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(0, fakeExecutor.runAllReady()); + } + + /** + * Test FakeExecutor that is told to delay execution on items. + */ + @Test + public void testDelayed() { + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + FakeExecutor fakeExecutor = new FakeExecutor(clock); + RunnableImpl runnable = new RunnableImpl(); + + // Add three delayed runnables. + fakeExecutor.executeDelayed(runnable, 1); + fakeExecutor.executeDelayed(runnable, 50); + fakeExecutor.executeDelayed(runnable, 100); + assertEquals(0, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(3, fakeExecutor.numPending()); + // Delayed runnables should not advance the clock and therefore should not run. + assertFalse(fakeExecutor.runNextReady()); + assertEquals(0, fakeExecutor.runAllReady()); + assertEquals(3, fakeExecutor.numPending()); + + // Advance the clock to the next runnable. One runnable should execute. + assertEquals(1, fakeExecutor.advanceClockToNext()); + assertEquals(1, fakeExecutor.runAllReady()); + assertEquals(2, fakeExecutor.numPending()); + assertEquals(1, runnable.mRunCount); + // Advance the clock to the last runnable. + assertEquals(99, fakeExecutor.advanceClockToLast()); + assertEquals(2, fakeExecutor.runAllReady()); + // Now all remaining runnables should execute. + assertEquals(0, fakeExecutor.numPending()); + assertEquals(3, runnable.mRunCount); + } + + /** + * Test FakeExecutor that is told to delay execution on items. + */ + @Test + public void testDelayed_AdvanceAndRun() { + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + FakeExecutor fakeExecutor = new FakeExecutor(clock); + RunnableImpl runnable = new RunnableImpl(); + + // Add three delayed runnables. + fakeExecutor.executeDelayed(runnable, 1); + fakeExecutor.executeDelayed(runnable, 50); + fakeExecutor.executeDelayed(runnable, 100); + assertEquals(0, runnable.mRunCount); + assertEquals(0, clock.uptimeMillis()); + assertEquals(3, fakeExecutor.numPending()); + // Delayed runnables should not advance the clock and therefore should not run. + assertFalse(fakeExecutor.runNextReady()); + assertEquals(0, fakeExecutor.runAllReady()); + assertEquals(3, fakeExecutor.numPending()); + + // Advance the clock to the next runnable. Check that it is run. + assertEquals(1, fakeExecutor.advanceClockToNext()); + assertEquals(1, fakeExecutor.runAllReady()); + assertEquals(1, clock.uptimeMillis()); + assertEquals(2, fakeExecutor.numPending()); + assertEquals(1, runnable.mRunCount); + assertEquals(49, fakeExecutor.advanceClockToNext()); + assertEquals(1, fakeExecutor.runAllReady()); + assertEquals(50, clock.uptimeMillis()); + assertEquals(1, fakeExecutor.numPending()); + assertEquals(2, runnable.mRunCount); + assertEquals(50, fakeExecutor.advanceClockToNext()); + assertEquals(1, fakeExecutor.runAllReady()); + assertEquals(100, clock.uptimeMillis()); + assertEquals(0, fakeExecutor.numPending()); + assertEquals(3, runnable.mRunCount); + + // Nothing left to do + assertEquals(0, fakeExecutor.advanceClockToNext()); + assertEquals(0, fakeExecutor.runAllReady()); + assertEquals(100, clock.uptimeMillis()); + assertEquals(0, fakeExecutor.numPending()); + assertEquals(3, runnable.mRunCount); + } + + /** + * Test execution order. + */ + @Test + public void testExecutionOrder() { + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + FakeExecutor fakeExecutor = new FakeExecutor(clock); + RunnableImpl runnableA = new RunnableImpl(); + RunnableImpl runnableB = new RunnableImpl(); + RunnableImpl runnableC = new RunnableImpl(); + RunnableImpl runnableD = new RunnableImpl(); + + Function4 checkRunCounts = + (Integer argA, Integer argB, Integer argC, Integer argD) -> { + assertEquals("RunnableA run count wrong", argA.intValue(), runnableA.mRunCount); + assertEquals("RunnableB run count wrong", argB.intValue(), runnableB.mRunCount); + assertEquals("RunnableC run count wrong", argC.intValue(), runnableC.mRunCount); + assertEquals("RunnableD run count wrong", argD.intValue(), runnableD.mRunCount); + return null; + }; + + assertEquals(0, clock.uptimeMillis()); + checkRunCounts.invoke(0, 0, 0, 0); + + fakeExecutor.execute(runnableA); + fakeExecutor.execute(runnableB); + fakeExecutor.execute(runnableC); + fakeExecutor.execute(runnableD); + + fakeExecutor.runNextReady(); + checkRunCounts.invoke(1, 0, 0, 0); + fakeExecutor.runNextReady(); + checkRunCounts.invoke(1, 1, 0, 0); + fakeExecutor.runNextReady(); + checkRunCounts.invoke(1, 1, 1, 0); + fakeExecutor.runNextReady(); + checkRunCounts.invoke(1, 1, 1, 1); + + fakeExecutor.executeDelayed(runnableA, 100); + fakeExecutor.execute(runnableB); + fakeExecutor.executeDelayed(runnableC, 50); + fakeExecutor.execute(runnableD); + + fakeExecutor.advanceClockToNext(); + fakeExecutor.runAllReady(); + checkRunCounts.invoke(1, 2, 1, 2); + fakeExecutor.advanceClockToNext(); + fakeExecutor.runAllReady(); + checkRunCounts.invoke(1, 2, 2, 2); + fakeExecutor.advanceClockToNext(); + fakeExecutor.runAllReady(); + checkRunCounts.invoke(2, 2, 2, 2); + + fakeExecutor.execute(runnableA); + fakeExecutor.executeAtTime(runnableB, 0); // this is in the past! + fakeExecutor.executeAtTime(runnableC, 1000); + fakeExecutor.executeAtTime(runnableD, 500); + + fakeExecutor.advanceClockToNext(); + fakeExecutor.runAllReady(); + checkRunCounts.invoke(3, 3, 2, 2); + fakeExecutor.advanceClockToNext(); + fakeExecutor.runAllReady(); + checkRunCounts.invoke(3, 3, 2, 3); + fakeExecutor.advanceClockToNext(); + fakeExecutor.runAllReady(); + checkRunCounts.invoke(3, 3, 3, 3); + } + + /** + * Test removing a single item. + */ + @Test + public void testRemoval_single() { + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + FakeExecutor fakeExecutor = new FakeExecutor(clock); + RunnableImpl runnable = new RunnableImpl(); + Runnable removeFunction; + + // Nothing to remove. + assertEquals(0, runnable.mRunCount); + assertEquals(0, fakeExecutor.numPending()); + + // Two pending items that have not yet run. + // We will try to remove the second item. + fakeExecutor.executeDelayed(runnable, 100); + removeFunction = fakeExecutor.executeDelayed(runnable, 200); + assertEquals(2, fakeExecutor.numPending()); + assertEquals(0, runnable.mRunCount); + + // Remove the item. + removeFunction.run(); + assertEquals(1, fakeExecutor.numPending()); + assertEquals(0, runnable.mRunCount); + + // One item to run. + fakeExecutor.advanceClockToLast(); + fakeExecutor.runAllReady(); + assertEquals(0, fakeExecutor.numPending()); + assertEquals(1, runnable.mRunCount); + + // Nothing to remove. + removeFunction.run(); + fakeExecutor.runAllReady(); + assertEquals(0, fakeExecutor.numPending()); + assertEquals(1, runnable.mRunCount); + } + + /** + * Test removing multiple items. + */ + @Test + public void testRemoval_multi() { + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + FakeExecutor fakeExecutor = new FakeExecutor(clock); + List removeFunctions = new ArrayList<>(); + RunnableImpl runnable = new RunnableImpl(); + + // Nothing to remove. + assertEquals(0, runnable.mRunCount); + assertEquals(0, fakeExecutor.numPending()); + + // Three pending items that have not yet run. + // We will try to remove the first and third items. + removeFunctions.add(fakeExecutor.executeDelayed(runnable, 100)); + fakeExecutor.executeDelayed(runnable, 200); + removeFunctions.add(fakeExecutor.executeDelayed(runnable, 300)); + assertEquals(3, fakeExecutor.numPending()); + assertEquals(0, runnable.mRunCount); + + // Remove the items. + removeFunctions.forEach(Runnable::run); + assertEquals(1, fakeExecutor.numPending()); + assertEquals(0, runnable.mRunCount); + + // One item to run. + fakeExecutor.advanceClockToLast(); + fakeExecutor.runAllReady(); + assertEquals(0, fakeExecutor.numPending()); + assertEquals(1, runnable.mRunCount); + + // Nothing to remove. + removeFunctions.forEach(Runnable::run); + assertEquals(0, fakeExecutor.numPending()); + assertEquals(1, runnable.mRunCount); + } + + /** + * Test removing everything + */ + @Test + public void testRemoval_all() { + FakeSystemClock clock = new FakeSystemClock(); + clock.setAutoIncrement(false); + FakeExecutor fakeExecutor = new FakeExecutor(clock); + RunnableImpl runnable = new RunnableImpl(); + + // Nothing to remove. + assertEquals(0, runnable.mRunCount); + assertEquals(0, fakeExecutor.numPending()); + + // Two pending items that have not yet run. + fakeExecutor.executeDelayed(runnable, 100); + fakeExecutor.executeDelayed(runnable, 200); + assertEquals(2, fakeExecutor.numPending()); + assertEquals(0, runnable.mRunCount); + + // Remove the items. + fakeExecutor.removeAll(); + + // Nothing to run + fakeExecutor.advanceClockToLast(); + assertEquals(0, fakeExecutor.runAllReady()); + assertEquals(0, fakeExecutor.numPending()); + assertEquals(0, runnable.mRunCount); + } + + private static class RunnableImpl implements Runnable { + int mRunCount; + + @Override + public void run() { + mRunCount++; + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java index 7b5417cd5c366..65e5902c84dfa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java @@ -16,6 +16,9 @@ package com.android.systemui.util.time; +import java.util.ArrayList; +import java.util.List; + public class FakeSystemClock implements SystemClock { private boolean mAutoIncrement = true; @@ -26,11 +29,13 @@ public class FakeSystemClock implements SystemClock { private long mCurrentThreadTimeMicro; private long mCurrentTimeMicro; + List mListeners = new ArrayList<>(); + @Override public long uptimeMillis() { long value = mUptimeMillis; if (mAutoIncrement) { - mUptimeMillis++; + setUptimeMillis(mUptimeMillis + 1); } return value; } @@ -39,7 +44,7 @@ public class FakeSystemClock implements SystemClock { public long elapsedRealtime() { long value = mElapsedRealtime; if (mAutoIncrement) { - mElapsedRealtime++; + setElapsedRealtime(mElapsedRealtime + 1); } return value; } @@ -48,7 +53,7 @@ public class FakeSystemClock implements SystemClock { public long elapsedRealtimeNanos() { long value = mElapsedRealtimeNanos; if (mAutoIncrement) { - mElapsedRealtimeNanos++; + setElapsedRealtimeNanos(mElapsedRealtimeNanos + 1); } return value; } @@ -57,7 +62,7 @@ public class FakeSystemClock implements SystemClock { public long currentThreadTimeMillis() { long value = mCurrentThreadTimeMillis; if (mAutoIncrement) { - mCurrentThreadTimeMillis++; + setCurrentThreadTimeMillis(mCurrentThreadTimeMillis + 1); } return value; } @@ -66,7 +71,7 @@ public class FakeSystemClock implements SystemClock { public long currentThreadTimeMicro() { long value = mCurrentThreadTimeMicro; if (mAutoIncrement) { - mCurrentThreadTimeMicro++; + setCurrentThreadTimeMicro(mCurrentThreadTimeMicro + 1); } return value; } @@ -75,37 +80,90 @@ public class FakeSystemClock implements SystemClock { public long currentTimeMicro() { long value = mCurrentTimeMicro; if (mAutoIncrement) { - mCurrentTimeMicro++; + setCurrentTimeMicro(mCurrentTimeMicro + 1); } return value; } public void setUptimeMillis(long uptimeMillis) { mUptimeMillis = uptimeMillis; + for (ClockTickListener listener : mListeners) { + listener.onUptimeMillis(mUptimeMillis); + } } public void setElapsedRealtime(long elapsedRealtime) { mElapsedRealtime = elapsedRealtime; + for (ClockTickListener listener : mListeners) { + listener.onElapsedRealtime(mElapsedRealtime); + } } public void setElapsedRealtimeNanos(long elapsedRealtimeNanos) { mElapsedRealtimeNanos = elapsedRealtimeNanos; + for (ClockTickListener listener : mListeners) { + listener.onElapsedRealtimeNanos(mElapsedRealtimeNanos); + } } public void setCurrentThreadTimeMillis(long currentThreadTimeMillis) { mCurrentThreadTimeMillis = currentThreadTimeMillis; + for (ClockTickListener listener : mListeners) { + listener.onCurrentThreadTimeMillis(mCurrentThreadTimeMillis); + } } public void setCurrentThreadTimeMicro(long currentThreadTimeMicro) { mCurrentThreadTimeMicro = currentThreadTimeMicro; + for (ClockTickListener listener : mListeners) { + listener.onCurrentThreadTimeMicro(mCurrentThreadTimeMicro); + } } public void setCurrentTimeMicro(long currentTimeMicro) { mCurrentTimeMicro = currentTimeMicro; + for (ClockTickListener listener : mListeners) { + listener.onCurrentTimeMicro(mCurrentTimeMicro); + } } /** If true, each call to get____ will be one higher than the previous call to that method. */ public void setAutoIncrement(boolean autoIncrement) { mAutoIncrement = autoIncrement; } + + public void addListener(ClockTickListener listener) { + mListeners.add(listener); + } + + public void removeListener(ClockTickListener listener) { + mListeners.remove(listener); + } + + /** Alert all the listeners about the current time. */ + public void synchronizeListeners() { + for (ClockTickListener listener : mListeners) { + listener.onUptimeMillis(mUptimeMillis); + listener.onElapsedRealtime(mElapsedRealtime); + listener.onElapsedRealtimeNanos(mElapsedRealtimeNanos); + listener.onCurrentThreadTimeMillis(mCurrentThreadTimeMillis); + listener.onCurrentThreadTimeMicro(mCurrentThreadTimeMicro); + listener.onCurrentTimeMicro(mCurrentTimeMicro); + } + } + + + public interface ClockTickListener { + default void onUptimeMillis(long uptimeMillis) {} + + default void onElapsedRealtime(long elapsedRealtime) {} + + default void onElapsedRealtimeNanos(long elapsedRealtimeNanos) {} + + default void onCurrentThreadTimeMillis(long currentThreadTimeMillis) {} + + default void onCurrentThreadTimeMicro(long currentThreadTimeMicro) {} + + default void onCurrentTimeMicro(long currentTimeMicro) {} + } }