diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java index d0fff749972db..64e56f9550582 100644 --- a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java +++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java @@ -41,6 +41,7 @@ import com.android.systemui.util.InjectionInflationController; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import javax.inject.Inject; import javax.inject.Singleton; @@ -52,14 +53,9 @@ import javax.inject.Singleton; public final class ClockManager { private static final String TAG = "ClockOptsProvider"; - private static final String DEFAULT_CLOCK_ID = "default"; - private final List mClockInfos = new ArrayList<>(); - /** - * Map from expected value stored in settings to supplier of custom clock face. - */ - private final Map mClocks = new ArrayMap<>(); - @Nullable private ClockPlugin mCurrentClock; + private final AvailableClocks mPreviewClocks; + private final List> mBuiltinClocks = new ArrayList<>(); private final Context mContext; private final ContentResolver mContentResolver; @@ -78,21 +74,6 @@ public final class ClockManager { } }; - private final PluginListener mClockPluginListener = - new PluginListener() { - @Override - public void onPluginConnected(ClockPlugin plugin, Context pluginContext) { - addClockPlugin(plugin); - reload(); - } - - @Override - public void onPluginDisconnected(ClockPlugin plugin) { - removeClockPlugin(plugin); - reload(); - } - }; - private final PluginManager mPluginManager; /** @@ -108,13 +89,22 @@ public final class ClockManager { } }; @Nullable private DockManager mDockManager; + /** * When docked, the DOCKED_CLOCK_FACE setting will be checked for the custom clock face * to show. */ private boolean mIsDocked; - private final List mListeners = new ArrayList<>(); + /** + * Listeners for onClockChanged event. + * + * Each listener must receive a separate clock plugin instance. Otherwise, there could be + * problems like attempting to attach a view that already has a parent. To deal with this issue, + * each listener is associated with a collection of available clocks. When onClockChanged is + * fired the current clock plugin instance is retrieved from that listeners available clocks. + */ + private final Map mListeners = new ArrayMap<>(); private final int mWidth; private final int mHeight; @@ -133,14 +123,16 @@ public final class ClockManager { mPluginManager = pluginManager; mContentResolver = contentResolver; mSettingsWrapper = settingsWrapper; + mPreviewClocks = new AvailableClocks(); Resources res = context.getResources(); LayoutInflater layoutInflater = injectionInflater.injectable(LayoutInflater.from(context)); - addClockPlugin(new DefaultClockController(res, layoutInflater, colorExtractor)); - addClockPlugin(new BubbleClockController(res, layoutInflater, colorExtractor)); - addClockPlugin(new StretchAnalogClockController(res, layoutInflater, colorExtractor)); - addClockPlugin(new TypeClockController(res, layoutInflater, colorExtractor)); + addBuiltinClock(() -> new DefaultClockController(res, layoutInflater, colorExtractor)); + addBuiltinClock(() -> new BubbleClockController(res, layoutInflater, colorExtractor)); + addBuiltinClock(() -> new StretchAnalogClockController(res, layoutInflater, + colorExtractor)); + addBuiltinClock(() -> new TypeClockController(res, layoutInflater, colorExtractor)); // Store the size of the display for generation of clock preview. DisplayMetrics dm = res.getDisplayMetrics(); @@ -155,7 +147,12 @@ public final class ClockManager { if (mListeners.isEmpty()) { register(); } - mListeners.add(listener); + AvailableClocks availableClocks = new AvailableClocks(); + for (int i = 0; i < mBuiltinClocks.size(); i++) { + availableClocks.addClockPlugin(mBuiltinClocks.get(i).get()); + } + mListeners.put(listener, availableClocks); + mPluginManager.addPluginListener(availableClocks, ClockPlugin.class, true); reload(); } @@ -163,7 +160,8 @@ public final class ClockManager { * Remove listener added with {@link addOnClockChangedListener}. */ public void removeOnClockChangedListener(ClockChangedListener listener) { - mListeners.remove(listener); + AvailableClocks availableClocks = mListeners.remove(listener); + mPluginManager.removePluginListener(availableClocks); if (mListeners.isEmpty()) { unregister(); } @@ -173,16 +171,16 @@ public final class ClockManager { * Get information about available clock faces. */ List getClockInfos() { - return mClockInfos; + return mPreviewClocks.getInfo(); } /** * Get the current clock. - * @returns current custom clock or null for default. + * @return current custom clock or null for default. */ @Nullable ClockPlugin getCurrentClock() { - return mCurrentClock; + return mPreviewClocks.getCurrentClock(); } @VisibleForTesting @@ -195,39 +193,14 @@ public final class ClockManager { return mContentObserver; } - private void addClockPlugin(ClockPlugin plugin) { - final String id = plugin.getClass().getName(); - mClocks.put(plugin.getClass().getName(), plugin); - mClockInfos.add(ClockInfo.builder() - .setName(plugin.getName()) - .setTitle(plugin.getTitle()) - .setId(id) - .setThumbnail(() -> plugin.getThumbnail()) - .setPreview(() -> plugin.getPreview(mWidth, mHeight)) - .build()); - } - - private void removeClockPlugin(ClockPlugin plugin) { - final String id = plugin.getClass().getName(); - mClocks.remove(id); - for (int i = 0; i < mClockInfos.size(); i++) { - if (id.equals(mClockInfos.get(i).getId())) { - mClockInfos.remove(i); - break; - } - } - } - - private void notifyClockChanged(ClockPlugin plugin) { - for (int i = 0; i < mListeners.size(); i++) { - // It probably doesn't make sense to supply the same plugin instances to multiple - // listeners. This should be fine for now since there is only a single listener. - mListeners.get(i).onClockChanged(plugin); - } + private void addBuiltinClock(Supplier pluginSupplier) { + ClockPlugin plugin = pluginSupplier.get(); + mPreviewClocks.addClockPlugin(plugin); + mBuiltinClocks.add(pluginSupplier); } private void register() { - mPluginManager.addPluginListener(mClockPluginListener, ClockPlugin.class, true); + mPluginManager.addPluginListener(mPreviewClocks, ClockPlugin.class, true); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), false, mContentObserver); @@ -243,7 +216,7 @@ public final class ClockManager { } private void unregister() { - mPluginManager.removePluginListener(mClockPluginListener); + mPluginManager.removePluginListener(mPreviewClocks); mContentResolver.unregisterContentObserver(mContentObserver); if (mDockManager != null) { mDockManager.removeListener(mDockEventListener); @@ -251,30 +224,16 @@ public final class ClockManager { } private void reload() { - mCurrentClock = getClockPlugin(); - if (mCurrentClock instanceof DefaultClockController) { - notifyClockChanged(null); - } else { - notifyClockChanged(mCurrentClock); - } - } - - private ClockPlugin getClockPlugin() { - ClockPlugin plugin = null; - if (mIsDocked) { - final String name = mSettingsWrapper.getDockedClockFace(); - if (name != null) { - plugin = mClocks.get(name); - if (plugin != null) { - return plugin; - } + mPreviewClocks.reload(); + mListeners.forEach((listener, clocks) -> { + clocks.reload(); + ClockPlugin clock = clocks.getCurrentClock(); + if (clock instanceof DefaultClockController) { + listener.onClockChanged(null); + } else { + listener.onClockChanged(clock); } - } - final String name = mSettingsWrapper.getLockScreenCustomClockFace(); - if (name != null) { - plugin = mClocks.get(name); - } - return plugin; + }); } /** @@ -288,4 +247,106 @@ public final class ClockManager { */ void onClockChanged(ClockPlugin clock); } + + /** + * Collection of available clocks. + */ + private final class AvailableClocks implements PluginListener { + + /** + * Map from expected value stored in settings to plugin for custom clock face. + */ + private final Map mClocks = new ArrayMap<>(); + + /** + * Metadata about available clocks, such as name and preview images. + */ + private final List mClockInfo = new ArrayList<>(); + + /** + * Active ClockPlugin. + */ + @Nullable private ClockPlugin mCurrentClock; + + @Override + public void onPluginConnected(ClockPlugin plugin, Context pluginContext) { + addClockPlugin(plugin); + reload(); + } + + @Override + public void onPluginDisconnected(ClockPlugin plugin) { + removeClockPlugin(plugin); + reload(); + } + + /** + * Get the current clock. + * @return current custom clock or null for default. + */ + @Nullable + ClockPlugin getCurrentClock() { + return mCurrentClock; + } + + /** + * Get information about available clock faces. + */ + List getInfo() { + return mClockInfo; + } + + /** + * Adds a clock plugin to the collection of available clocks. + * + * @param plugin The plugin to add. + */ + void addClockPlugin(ClockPlugin plugin) { + final String id = plugin.getClass().getName(); + mClocks.put(plugin.getClass().getName(), plugin); + mClockInfo.add(ClockInfo.builder() + .setName(plugin.getName()) + .setTitle(plugin.getTitle()) + .setId(id) + .setThumbnail(plugin::getThumbnail) + .setPreview(() -> plugin.getPreview(mWidth, mHeight)) + .build()); + } + + private void removeClockPlugin(ClockPlugin plugin) { + final String id = plugin.getClass().getName(); + mClocks.remove(id); + for (int i = 0; i < mClockInfo.size(); i++) { + if (id.equals(mClockInfo.get(i).getId())) { + mClockInfo.remove(i); + break; + } + } + } + + /** + * Update the current clock. + */ + void reload() { + mCurrentClock = getClockPlugin(); + } + + private ClockPlugin getClockPlugin() { + ClockPlugin plugin = null; + if (ClockManager.this.isDocked()) { + final String name = mSettingsWrapper.getDockedClockFace(); + if (name != null) { + plugin = mClocks.get(name); + if (plugin != null) { + return plugin; + } + } + } + final String name = mSettingsWrapper.getLockScreenCustomClockFace(); + if (name != null) { + plugin = mClocks.get(name); + } + return plugin; + } + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java index 46b1833c2d6ce..f2ad958c57aba 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java @@ -18,6 +18,8 @@ package com.android.keyguard.clock; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ContentResolver; @@ -31,6 +33,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerFake; +import com.android.systemui.plugins.ClockPlugin; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.util.InjectionInflationController; @@ -38,6 +41,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -57,7 +61,8 @@ public final class ClockManagerTest extends SysuiTestCase { @Mock SysuiColorExtractor mMockColorExtractor; @Mock ContentResolver mMockContentResolver; @Mock SettingsWrapper mMockSettingsWrapper; - @Mock ClockManager.ClockChangedListener mMockListener; + @Mock ClockManager.ClockChangedListener mMockListener1; + @Mock ClockManager.ClockChangedListener mMockListener2; @Before public void setUp() { @@ -73,13 +78,17 @@ public final class ClockManagerTest extends SysuiTestCase { mMockPluginManager, mMockColorExtractor, mMockContentResolver, mMockSettingsWrapper); - mClockManager.addOnClockChangedListener(mMockListener); + mClockManager.addOnClockChangedListener(mMockListener1); + mClockManager.addOnClockChangedListener(mMockListener2); + reset(mMockListener1, mMockListener2); + mContentObserver = mClockManager.getContentObserver(); } @After public void tearDown() { - mClockManager.removeOnClockChangedListener(mMockListener); + mClockManager.removeOnClockChangedListener(mMockListener1); + mClockManager.removeOnClockChangedListener(mMockListener2); } @Test @@ -115,6 +124,34 @@ public final class ClockManagerTest extends SysuiTestCase { assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS); } + @Test + public void onClockChanged_customClock() { + // GIVEN that settings is set to the bubble clock face + when(mMockSettingsWrapper.getLockScreenCustomClockFace()).thenReturn(BUBBLE_CLOCK); + // WHEN settings change event is fired + mContentObserver.onChange(false); + // THEN the plugin is the bubble clock face. + ArgumentCaptor captor = ArgumentCaptor.forClass(ClockPlugin.class); + verify(mMockListener1).onClockChanged(captor.capture()); + assertThat(captor.getValue()).isInstanceOf(BUBBLE_CLOCK_CLASS); + } + + @Test + public void onClockChanged_uniqueInstances() { + // GIVEN that settings is set to the bubble clock face + when(mMockSettingsWrapper.getLockScreenCustomClockFace()).thenReturn(BUBBLE_CLOCK); + // WHEN settings change event is fired + mContentObserver.onChange(false); + // THEN the listeners receive separate instances of the Bubble clock plugin. + ArgumentCaptor captor1 = ArgumentCaptor.forClass(ClockPlugin.class); + ArgumentCaptor captor2 = ArgumentCaptor.forClass(ClockPlugin.class); + verify(mMockListener1).onClockChanged(captor1.capture()); + verify(mMockListener2).onClockChanged(captor2.capture()); + assertThat(captor1.getValue()).isInstanceOf(BUBBLE_CLOCK_CLASS); + assertThat(captor2.getValue()).isInstanceOf(BUBBLE_CLOCK_CLASS); + assertThat(captor1.getValue()).isNotSameAs(captor2.getValue()); + } + @Test public void getCurrentClock_badSettingsValue() { // GIVEN that settings contains a value that doesn't correspond to a