diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouter.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouter.java new file mode 100644 index 0000000000000..542cf6559c64d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouter.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2021 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; + +/** + * Allows triggering methods based on a passed in id or message, generally on another thread. + * + * Messages sent on to this router must be processed in order. That is to say, if three + * messages are sent with no delay, they must be processed in the order they were sent. Moreover, + * if messages are sent with various delays, they must be processed in order of their delay. + * + * Messages can be passed by either a simple integer or an instance of a class. Unique integers are + * considered unique messages. Unique message classes (not instances) are considered unique + * messages. You can use message classes to pass extra data for processing to subscribers. + * + *
+ *      // Three messages with three unique integer messages.
+ *      // They can be subscribed to independently.
+ *      router.sendMessage(0);
+ *      router.sendMessage(1);
+ *      router.sendMessage(2);
+ *
+ *      // Three messages with two unique message classes.
+ *      // The first and third messages will be delivered to the same subscribers.
+ *      router.sendMessage(new Foo(0));
+ *      router.sendMessage(new Bar(1));
+ *      router.sendMessage(new Foo(2));
+ *  
+ * + * The number of unique ids and message types used should be relatively constrained. Construct + * a custom message-class and put unique, per-message data inside of it. + */ +public interface MessageRouter { + /** + * Alerts any listeners subscribed to the passed in id. + * + * The number of unique ids used should be relatively constrained - used to identify the type + * of message being sent. If unique information needs to be passed with each call, use + * {@link #sendMessage(Object)}. + * + * @param id An identifier for the message + */ + default void sendMessage(int id) { + sendMessageDelayed(id, 0); + } + + /** + * Alerts any listeners subscribed to the passed in message. + * + * The number of message types used should be relatively constrained. If no unique information + * needs to be passed in, you can simply use {@link #sendMessage(int)}} which takes an integer + * instead of a unique class type. + * + * The class of the passed in object will be used to router the message. + * + * @param data A message containing extra data for processing. + */ + default void sendMessage(Object data) { + sendMessageDelayed(data, 0); + } + + /** + * Alerts any listeners subscribed to the passed in id in the future. + * + * The number of unique ids used should be relatively constrained - used to identify the type + * of message being sent. If unique information needs to be passed with each call, use + * {@link #sendMessageDelayed(Object, long)}. + * + * @param id An identifier for the message + * @param delayMs Number of milliseconds to wait before alerting. + */ + void sendMessageDelayed(int id, long delayMs); + + + /** + * Alerts any listeners subscribed to the passed in message in the future. + * + * The number of message types used should be relatively constrained. If no unique information + * needs to be passed in, you can simply use {@link #sendMessageDelayed(int, long)} which takes + * an integer instead of a unique class type. + * + * @param data A message containing extra data for processing. + * @param delayMs Number of milliseconds to wait before alerting. + */ + void sendMessageDelayed(Object data, long delayMs); + + /** + * Cancel all unprocessed messages for a given id. + * + * If a message has multiple listeners and one of those listeners has been alerted, the other + * listeners that follow it may also be alerted. This is only guaranteed to cancel messages + * that are still queued. + * + * @param id The message id to cancel. + */ + void cancelMessages(int id); + + /** + * Cancel all unprocessed messages for a given message type. + * + * If a message has multiple listeners and one of those listeners has been alerted, the other + * listeners that follow it may also be alerted. This is only guaranteed to cancel messages + * that are still queued. + * + * @param messageType The class of the message to cancel + */ + void cancelMessages(Class messageType); + + /** + * Add a listener for a message that does not handle any extra data. + * + * See also {@link #subscribeTo(Class, DataMessageListener)}. + * + * @param id The message id to listener for. + * @param listener + */ + void subscribeTo(int id, SimpleMessageListener listener); + + /** + * Add a listener for a message of a specific type. + * + * See also {@link #subscribeTo(Class, DataMessageListener)}. + * + * @param messageType The class of message to listen for. + * @param listener + */ + void subscribeTo(Class messageType, DataMessageListener listener); + + /** + * Remove a listener for a specific message. + * + * See also {@link #unsubscribeFrom(Class, DataMessageListener)} + * + * @param id The message id to stop listening for. + * @param listener The listener to remove. + */ + void unsubscribeFrom(int id, SimpleMessageListener listener); + + /** + * Remove a listener for a specific message. + * + * See also {@link #unsubscribeFrom(int, SimpleMessageListener)}. + * + * @param messageType The class of message to stop listening for. + * @param listener The listener to remove. + */ + void unsubscribeFrom(Class messageType, DataMessageListener listener); + + /** + * Remove a listener for all messages that it is subscribed to. + * + * See also {@link #unsubscribeFrom(DataMessageListener)}. + * + * @param listener The listener to remove. + */ + void unsubscribeFrom(SimpleMessageListener listener); + + /** + * Remove a listener for all messages that it is subscribed to. + * + * See also {@link #unsubscribeFrom(SimpleMessageListener)}. + * + * @param listener The listener to remove. + */ + void unsubscribeFrom(DataMessageListener listener); + + /** + * A Listener interface for when no extra data is expected or desired. + */ + interface SimpleMessageListener { + /** */ + void onMessage(int id); + } + + /** + * A Listener interface for when extra data is expected or desired. + * + * @param + */ + interface DataMessageListener { + /** */ + void onMessage(T data); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouterImpl.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouterImpl.java new file mode 100644 index 0000000000000..7145c775fe399 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouterImpl.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2021 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of {@link MessageRouter}. + */ +public class MessageRouterImpl implements MessageRouter { + + private final DelayableExecutor mDelayableExecutor; + + private final Map> mIdMessageCancelers = new HashMap<>(); + private final Map, List> mDataMessageCancelers = new HashMap<>(); + private final Map> mSimpleMessageListenerMap = + new HashMap<>(); + private final Map, List>> mDataMessageListenerMap = + new HashMap<>(); + + public MessageRouterImpl(DelayableExecutor delayableExecutor) { + mDelayableExecutor = delayableExecutor; + } + + @Override + public void sendMessageDelayed(int id, long delayMs) { + addCanceler(id, mDelayableExecutor.executeDelayed(() -> onMessage(id), delayMs)); + } + + @Override + public void sendMessageDelayed(Object data, long delayMs) { + addCanceler((Class) data.getClass(), mDelayableExecutor.executeDelayed( + () -> onMessage(data), delayMs)); + } + + @Override + public void cancelMessages(int id) { + synchronized (mIdMessageCancelers) { + if (mIdMessageCancelers.containsKey(id)) { + for (Runnable canceler : mIdMessageCancelers.get(id)) { + canceler.run(); + } + // Remove, don't clear, otherwise this could look like a memory leak as + // more and more unique message ids are passed in. + mIdMessageCancelers.remove(id); + } + } + } + + @Override + public void cancelMessages(Class messageType) { + synchronized (mDataMessageCancelers) { + if (mDataMessageCancelers.containsKey(messageType)) { + for (Runnable canceler : mDataMessageCancelers.get(messageType)) { + canceler.run(); + } + // Remove, don't clear, otherwise this could look like a memory leak as + // more and more unique message types are passed in. + mDataMessageCancelers.remove(messageType); + } + } + } + + @Override + public void subscribeTo(int id, SimpleMessageListener listener) { + synchronized (mSimpleMessageListenerMap) { + mSimpleMessageListenerMap.putIfAbsent(id, new ArrayList<>()); + mSimpleMessageListenerMap.get(id).add(listener); + } + } + + @Override + public void subscribeTo(Class messageType, DataMessageListener listener) { + synchronized (mDataMessageListenerMap) { + mDataMessageListenerMap.putIfAbsent(messageType, new ArrayList<>()); + mDataMessageListenerMap.get(messageType).add((DataMessageListener) listener); + } + } + + @Override + public void unsubscribeFrom(int id, SimpleMessageListener listener) { + synchronized (mSimpleMessageListenerMap) { + if (mSimpleMessageListenerMap.containsKey(id)) { + mSimpleMessageListenerMap.get(id).remove(listener); + } + } + } + + @Override + public void unsubscribeFrom(Class messageType, DataMessageListener listener) { + synchronized (mDataMessageListenerMap) { + if (mDataMessageListenerMap.containsKey(messageType)) { + mDataMessageListenerMap.get(messageType).remove(listener); + } + } + } + + @Override + public void unsubscribeFrom(SimpleMessageListener listener) { + synchronized (mSimpleMessageListenerMap) { + for (Integer id : mSimpleMessageListenerMap.keySet()) { + mSimpleMessageListenerMap.get(id).remove(listener); + } + } + } + + @Override + public void unsubscribeFrom(DataMessageListener listener) { + synchronized (mDataMessageListenerMap) { + for (Class messageType : mDataMessageListenerMap.keySet()) { + mDataMessageListenerMap.get(messageType).remove(listener); + } + } + } + + private void addCanceler(int id, Runnable canceler) { + synchronized (mIdMessageCancelers) { + mIdMessageCancelers.putIfAbsent(id, new ArrayList<>()); + mIdMessageCancelers.get(id).add(canceler); + } + } + + private void addCanceler(Class data, Runnable canceler) { + synchronized (mDataMessageCancelers) { + mDataMessageCancelers.putIfAbsent(data, new ArrayList<>()); + mDataMessageCancelers.get(data).add(canceler); + } + } + + private void onMessage(int id) { + synchronized (mSimpleMessageListenerMap) { + if (mSimpleMessageListenerMap.containsKey(id)) { + for (SimpleMessageListener listener : mSimpleMessageListenerMap.get(id)) { + listener.onMessage(id); + } + } + } + + synchronized (mIdMessageCancelers) { + if (mIdMessageCancelers.containsKey(id) && !mIdMessageCancelers.get(id).isEmpty()) { + mIdMessageCancelers.get(id).remove(0); + if (mIdMessageCancelers.get(id).isEmpty()) { + mIdMessageCancelers.remove(id); + } + } + } + } + + private void onMessage(Object data) { + synchronized (mDataMessageListenerMap) { + if (mDataMessageListenerMap.containsKey(data.getClass())) { + for (DataMessageListener listener : mDataMessageListenerMap.get( + data.getClass())) { + listener.onMessage(data); + } + } + } + + synchronized (mDataMessageCancelers) { + if (mDataMessageCancelers.containsKey(data.getClass()) + && !mDataMessageCancelers.get(data.getClass()).isEmpty()) { + mDataMessageCancelers.get(data.getClass()).remove(0); + if (mDataMessageCancelers.get(data.getClass()).isEmpty()) { + mDataMessageCancelers.remove(data.getClass()); + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java index b9b20c73c5d57..e9e794ea884b6 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java @@ -170,4 +170,20 @@ public abstract class SysUIConcurrencyModule { public static Executor provideUiBackgroundExecutor() { return Executors.newSingleThreadExecutor(); } + + /** */ + @Provides + @Main + public static MessageRouter providesMainMessageRouter( + @Main DelayableExecutor executor) { + return new MessageRouterImpl(executor); + } + + /** */ + @Provides + @Background + public static MessageRouter providesBackgroundMessageRouter( + @Background DelayableExecutor executor) { + return new MessageRouterImpl(executor); + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java index edea3055a783a..c7998882876ac 100644 --- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java @@ -36,7 +36,6 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Handler; import android.os.Looper; -import android.os.Message; import android.os.Process; import android.os.SystemProperties; import android.provider.Settings; @@ -60,6 +59,8 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSIconViewImpl; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.concurrency.MessageRouter; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -110,18 +111,18 @@ public class GarbageMonitor implements Dumpable { private static final int DO_GARBAGE_INSPECTION = 1000; private static final int DO_HEAP_TRACK = 3000; - private static final int GARBAGE_ALLOWANCE = 5; + static final int GARBAGE_ALLOWANCE = 5; private static final String TAG = "GarbageMonitor"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private final Handler mHandler; + private final MessageRouter mMessageRouter; private final TrackedGarbage mTrackedGarbage; private final LeakReporter mLeakReporter; private final Context mContext; - private final ActivityManager mAm; + private final DelayableExecutor mDelayableExecutor; private MemoryTile mQSTile; - private DumpTruck mDumpTruck; + private final DumpTruck mDumpTruck; private final LongSparseArray mData = new LongSparseArray<>(); private final ArrayList mPids = new ArrayList<>(); @@ -133,13 +134,16 @@ public class GarbageMonitor implements Dumpable { @Inject public GarbageMonitor( Context context, - @Background Looper bgLooper, + @Background DelayableExecutor delayableExecutor, + @Background MessageRouter messageRouter, LeakDetector leakDetector, LeakReporter leakReporter) { mContext = context.getApplicationContext(); - mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - mHandler = new BackgroundHeapCheckHandler(bgLooper); + mDelayableExecutor = delayableExecutor; + mMessageRouter = messageRouter; + mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection); + mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack); mTrackedGarbage = leakDetector.getTrackedGarbage(); mLeakReporter = leakReporter; @@ -158,13 +162,13 @@ public class GarbageMonitor implements Dumpable { return; } - mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION); + mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION); } public void startHeapTracking() { startTrackingProcess( android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis()); - mHandler.sendEmptyMessage(DO_HEAP_TRACK); + mMessageRouter.sendMessage(DO_HEAP_TRACK); } private boolean gcAndCheckGarbage() { @@ -586,33 +590,18 @@ public class GarbageMonitor implements Dumpable { } } - private class BackgroundHeapCheckHandler extends Handler { - BackgroundHeapCheckHandler(Looper onLooper) { - super(onLooper); - if (Looper.getMainLooper().equals(onLooper)) { - throw new RuntimeException( - "BackgroundHeapCheckHandler may not run on the ui thread"); - } + private void doGarbageInspection(int id) { + if (gcAndCheckGarbage()) { + mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100); } - @Override - public void handleMessage(Message m) { - switch (m.what) { - case DO_GARBAGE_INSPECTION: - if (gcAndCheckGarbage()) { - postDelayed(GarbageMonitor.this::reinspectGarbageAfterGc, 100); - } + mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION); + mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL); + } - removeMessages(DO_GARBAGE_INSPECTION); - sendEmptyMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL); - break; - - case DO_HEAP_TRACK: - update(); - removeMessages(DO_HEAP_TRACK); - sendEmptyMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL); - break; - } - } + private void doHeapTrack(int id) { + update(); + mMessageRouter.cancelMessages(DO_HEAP_TRACK); + mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MessageRouterImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MessageRouterImplTest.java new file mode 100644 index 0000000000000..78fc6803ea7ec --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MessageRouterImplTest.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2021 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +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 org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class MessageRouterImplTest extends SysuiTestCase { + private static final int MESSAGE_A = 0; + private static final int MESSAGE_B = 1; + private static final int MESSAGE_C = 2; + + private static final String METADATA_A = "A"; + private static final String METADATA_B = "B"; + private static final String METADATA_C = "C"; + private static final Foobar METADATA_FOO = new Foobar(); + + FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + @Mock + MessageRouter.SimpleMessageListener mNoMdListener; + @Mock + MessageRouter.DataMessageListener mStringListener; + @Mock + MessageRouter.DataMessageListener mFoobarListener; + private MessageRouterImpl mMR; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mMR = new MessageRouterImpl(mFakeExecutor); + } + + @Test + public void testSingleMessage_NoMetaData() { + mMR.subscribeTo(MESSAGE_A, mNoMdListener); + + mMR.sendMessage(MESSAGE_A); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener).onMessage(MESSAGE_A); + } + + @Test + public void testSingleMessage_WithMetaData() { + mMR.subscribeTo(String.class, mStringListener); + + mMR.sendMessage(METADATA_A); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.runAllReady(); + verify(mStringListener).onMessage(METADATA_A); + } + + @Test + public void testMessages_WithMixedMetaData() { + mMR.subscribeTo(String.class, mStringListener); + mMR.subscribeTo(Foobar.class, mFoobarListener); + + mMR.sendMessage(METADATA_A); + verify(mStringListener, never()).onMessage(anyString()); + verify(mFoobarListener, never()).onMessage(any(Foobar.class)); + + mFakeExecutor.runAllReady(); + verify(mStringListener).onMessage(METADATA_A); + verify(mFoobarListener, never()).onMessage(any(Foobar.class)); + + reset(mStringListener); + reset(mFoobarListener); + + mMR.sendMessage(METADATA_FOO); + verify(mStringListener, never()).onMessage(anyString()); + verify(mFoobarListener, never()).onMessage(any(Foobar.class)); + + mFakeExecutor.runAllReady(); + verify(mStringListener, never()).onMessage(anyString()); + verify(mFoobarListener).onMessage(METADATA_FOO); + } + + @Test + public void testMessages_WithAndWithoutMetaData() { + mMR.subscribeTo(MESSAGE_A, mNoMdListener); + mMR.subscribeTo(String.class, mStringListener); + + mMR.sendMessage(MESSAGE_A); + verify(mNoMdListener, never()).onMessage(anyInt()); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener).onMessage(MESSAGE_A); + verify(mStringListener, never()).onMessage(anyString()); + + reset(mNoMdListener); + reset(mStringListener); + + mMR.sendMessage(METADATA_A); + verify(mNoMdListener, never()).onMessage(anyInt()); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener, never()).onMessage(anyInt()); + verify(mStringListener).onMessage(METADATA_A); + } + + @Test + public void testRepeatedMessage() { + mMR.subscribeTo(String.class, mStringListener); + + mMR.sendMessage(METADATA_A); + mMR.sendMessage(METADATA_A); + mMR.sendMessage(METADATA_B); + verify(mStringListener, never()).onMessage(anyString()); + + InOrder ordered = inOrder(mStringListener); + mFakeExecutor.runNextReady(); + ordered.verify(mStringListener).onMessage(METADATA_A); + mFakeExecutor.runNextReady(); + ordered.verify(mStringListener).onMessage(METADATA_A); + mFakeExecutor.runNextReady(); + ordered.verify(mStringListener).onMessage(METADATA_B); + } + + @Test + public void testCancelMessage() { + mMR.subscribeTo(MESSAGE_A, mNoMdListener); + mMR.subscribeTo(MESSAGE_B, mNoMdListener); + mMR.subscribeTo(MESSAGE_C, mNoMdListener); + + mMR.sendMessage(MESSAGE_A); + mMR.sendMessage(MESSAGE_B); + mMR.sendMessage(MESSAGE_B); + mMR.sendMessage(MESSAGE_B); + mMR.sendMessage(MESSAGE_B); + mMR.sendMessage(MESSAGE_C); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mMR.cancelMessages(MESSAGE_B); + + mFakeExecutor.runAllReady(); + + InOrder ordered = inOrder(mNoMdListener); + ordered.verify(mNoMdListener).onMessage(MESSAGE_A); + ordered.verify(mNoMdListener).onMessage(MESSAGE_C); + } + + @Test + public void testSendMessage_NoSubscriber() { + mMR.sendMessage(MESSAGE_A); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mMR.subscribeTo(MESSAGE_A, mNoMdListener); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mMR.sendMessage(MESSAGE_A); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener).onMessage(MESSAGE_A); + } + + @Test + public void testUnsubscribe_SpecificMessage_NoMetadata() { + mMR.subscribeTo(MESSAGE_A, mNoMdListener); + mMR.subscribeTo(MESSAGE_B, mNoMdListener); + mMR.sendMessage(MESSAGE_A); + mMR.sendMessage(MESSAGE_B); + + mFakeExecutor.runAllReady(); + InOrder ordered = inOrder(mNoMdListener); + ordered.verify(mNoMdListener).onMessage(MESSAGE_A); + ordered.verify(mNoMdListener).onMessage(MESSAGE_B); + + reset(mNoMdListener); + mMR.unsubscribeFrom(MESSAGE_A, mNoMdListener); + mMR.sendMessage(MESSAGE_A); + mMR.sendMessage(MESSAGE_B); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener, never()).onMessage(MESSAGE_A); + verify(mNoMdListener).onMessage(MESSAGE_B); + } + + @Test + public void testUnsubscribe_SpecificMessage_WithMetadata() { + mMR.subscribeTo(String.class, mStringListener); + mMR.subscribeTo(Foobar.class, mFoobarListener); + mMR.sendMessage(METADATA_A); + mMR.sendMessage(METADATA_FOO); + + mFakeExecutor.runNextReady(); + verify(mStringListener).onMessage(METADATA_A); + mFakeExecutor.runNextReady(); + verify(mFoobarListener).onMessage(METADATA_FOO); + + reset(mStringListener); + reset(mFoobarListener); + mMR.unsubscribeFrom(String.class, mStringListener); + mMR.sendMessage(METADATA_A); + mMR.sendMessage(METADATA_FOO); + + mFakeExecutor.runAllReady(); + verify(mStringListener, never()).onMessage(METADATA_A); + verify(mFoobarListener).onMessage(METADATA_FOO); + } + + @Test + public void testUnsubscribe_AllMessages_NoMetadata() { + mMR.subscribeTo(MESSAGE_A, mNoMdListener); + mMR.subscribeTo(MESSAGE_B, mNoMdListener); + mMR.subscribeTo(MESSAGE_C, mNoMdListener); + + mMR.sendMessage(MESSAGE_A); + mMR.sendMessage(MESSAGE_B); + mMR.sendMessage(MESSAGE_C); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener).onMessage(MESSAGE_A); + verify(mNoMdListener).onMessage(MESSAGE_B); + verify(mNoMdListener).onMessage(MESSAGE_C); + + reset(mNoMdListener); + + mMR.unsubscribeFrom(mNoMdListener); + mMR.sendMessage(MESSAGE_A); + mMR.sendMessage(MESSAGE_B); + mMR.sendMessage(MESSAGE_C); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener, never()).onMessage(MESSAGE_A); + verify(mNoMdListener, never()).onMessage(MESSAGE_B); + verify(mNoMdListener, never()).onMessage(MESSAGE_C); + } + + @Test + public void testUnsubscribe_AllMessages_WithMetadata() { + mMR.subscribeTo(String.class, mStringListener); + + mMR.sendMessage(METADATA_A); + mMR.sendMessage(METADATA_B); + mMR.sendMessage(METADATA_C); + + mFakeExecutor.runAllReady(); + verify(mStringListener).onMessage(METADATA_A); + verify(mStringListener).onMessage(METADATA_B); + verify(mStringListener).onMessage(METADATA_C); + + reset(mStringListener); + + mMR.unsubscribeFrom(mStringListener); + mMR.sendMessage(METADATA_A); + mMR.sendMessage(METADATA_B); + mMR.sendMessage(METADATA_C); + + mFakeExecutor.runAllReady(); + verify(mStringListener, never()).onMessage(METADATA_A); + verify(mStringListener, never()).onMessage(METADATA_B); + verify(mStringListener, never()).onMessage(METADATA_C); + } + + @Test + public void testSingleDelayedMessage_NoMetaData() { + mMR.subscribeTo(MESSAGE_A, mNoMdListener); + + mMR.sendMessageDelayed(MESSAGE_A, 100); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mFakeExecutor.runAllReady(); + verify(mNoMdListener, never()).onMessage(anyInt()); + + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runAllReady(); + verify(mNoMdListener).onMessage(MESSAGE_A); + } + + @Test + public void testSingleDelayedMessage_WithMetaData() { + mMR.subscribeTo(String.class, mStringListener); + + mMR.sendMessageDelayed(METADATA_C, 1000); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.runAllReady(); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runAllReady(); + verify(mStringListener).onMessage(METADATA_C); + } + + @Test + public void testMultipleDelayedMessages() { + mMR.subscribeTo(String.class, mStringListener); + + mMR.sendMessageDelayed(METADATA_A, 100); + mMR.sendMessageDelayed(METADATA_B, 1000); + mMR.sendMessageDelayed(METADATA_C, 500); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.runAllReady(); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); + + InOrder ordered = inOrder(mStringListener); + ordered.verify(mStringListener).onMessage(METADATA_A); + ordered.verify(mStringListener).onMessage(METADATA_C); + ordered.verify(mStringListener).onMessage(METADATA_B); + } + + @Test + public void testCancelDelayedMessages() { + mMR.subscribeTo(String.class, mStringListener); + + mMR.sendMessageDelayed(METADATA_A, 100); + mMR.sendMessageDelayed(METADATA_B, 1000); + mMR.sendMessageDelayed(METADATA_C, 500); + verify(mStringListener, never()).onMessage(anyString()); + + mFakeExecutor.runAllReady(); + verify(mStringListener, never()).onMessage(anyString()); + + mMR.cancelMessages(String.class); + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); + + verify(mStringListener, never()).onMessage(anyString()); + } + + private static class Foobar {} +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java index bcc20c2d1b378..724d14e203745 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java @@ -17,124 +17,94 @@ package com.android.systemui.util.leak; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.Context; -import android.os.Looper; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.concurrency.MessageRouterImpl; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) -@RunWithLooper public class GarbageMonitorTest extends SysuiTestCase { - private LeakReporter mLeakReporter; - private TrackedGarbage mTrackedGarbage; - private TestableGarbageMonitor mGarbageMonitor; + @Mock private LeakReporter mLeakReporter; + @Mock private TrackedGarbage mTrackedGarbage; + private GarbageMonitor mGarbageMonitor; + private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); @Before public void setup() { - mTrackedGarbage = mock(TrackedGarbage.class); - mLeakReporter = mock(LeakReporter.class); + MockitoAnnotations.initMocks(this); mGarbageMonitor = - new TestableGarbageMonitor( + new GarbageMonitor( mContext, - TestableLooper.get(this).getLooper(), + mFakeExecutor, + new MessageRouterImpl(mFakeExecutor), new LeakDetector(null, mTrackedGarbage, null), mLeakReporter); } - @Test - public void testCallbacks_getScheduled() { - mGarbageMonitor.startLeakMonitor(); - mGarbageMonitor.runCallbacksOnce(); - mGarbageMonitor.runCallbacksOnce(); - mGarbageMonitor.runCallbacksOnce(); - } - - @Test - public void testNoGarbage_doesntDump() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(0); - - mGarbageMonitor.startLeakMonitor(); - mGarbageMonitor.runCallbacksOnce(); - mGarbageMonitor.runCallbacksOnce(); - mGarbageMonitor.runCallbacksOnce(); - - verify(mLeakReporter, never()).dumpLeak(anyInt()); - } - @Test public void testALittleGarbage_doesntDump() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(4); + when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE); - mGarbageMonitor.startLeakMonitor(); - mGarbageMonitor.runCallbacksOnce(); - mGarbageMonitor.runCallbacksOnce(); - mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.reinspectGarbageAfterGc(); verify(mLeakReporter, never()).dumpLeak(anyInt()); } @Test public void testTransientGarbage_doesntDump() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(100); + when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1); + // Start the leak monitor. Nothing gets reported immediately. mGarbageMonitor.startLeakMonitor(); - mGarbageMonitor.runInspectCallback(); + mFakeExecutor.runAllReady(); + verify(mLeakReporter, never()).dumpLeak(anyInt()); + // Garbage gets reset to 0 before the leak reporte actually gets called. when(mTrackedGarbage.countOldGarbage()).thenReturn(0); + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); - mGarbageMonitor.runReinspectCallback(); - + // Therefore nothing gets dumped. verify(mLeakReporter, never()).dumpLeak(anyInt()); } @Test public void testLotsOfPersistentGarbage_dumps() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(100); + when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1); - mGarbageMonitor.startLeakMonitor(); - mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.reinspectGarbageAfterGc(); - verify(mLeakReporter).dumpLeak(anyInt()); + verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1); } - private static class TestableGarbageMonitor extends GarbageMonitor { - public TestableGarbageMonitor( - Context context, - Looper looper, - LeakDetector leakDetector, - LeakReporter leakReporter) { - super(context, looper, leakDetector, leakReporter); - } + @Test + public void testLotsOfPersistentGarbage_dumpsAfterAtime() { + when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1); - void runInspectCallback() { - startLeakMonitor(); - } + // Start the leak monitor. Nothing gets reported immediately. + mGarbageMonitor.startLeakMonitor(); + mFakeExecutor.runAllReady(); + verify(mLeakReporter, never()).dumpLeak(anyInt()); - void runReinspectCallback() { - reinspectGarbageAfterGc(); - } + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); - void runCallbacksOnce() { - // Note that TestableLooper doesn't currently support delayed messages so we need to run - // callbacks explicitly. - runInspectCallback(); - runReinspectCallback(); - } + verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1); } } \ No newline at end of file