Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ad8d508d authored by Lucas Dupin's avatar Lucas Dupin Committed by Android (Google) Code Review
Browse files

Merge "ClockManager listeners receive separate ClockPlugin instances."

parents 4c26bb67 d8445eae
Loading
Loading
Loading
Loading
+147 −86
Original line number Diff line number Diff line
@@ -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<ClockInfo> mClockInfos = new ArrayList<>();
    /**
     * Map from expected value stored in settings to supplier of custom clock face.
     */
    private final Map<String, ClockPlugin> mClocks = new ArrayMap<>();
    @Nullable private ClockPlugin mCurrentClock;
    private final AvailableClocks mPreviewClocks;
    private final List<Supplier<ClockPlugin>> mBuiltinClocks = new ArrayList<>();

    private final Context mContext;
    private final ContentResolver mContentResolver;
@@ -78,21 +74,6 @@ public final class ClockManager {
                }
            };

    private final PluginListener<ClockPlugin> mClockPluginListener =
            new PluginListener<ClockPlugin>() {
                @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<ClockChangedListener> 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<ClockChangedListener, AvailableClocks> 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<ClockInfo> 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<ClockPlugin> 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,17 +224,116 @@ public final class ClockManager {
    }

    private void reload() {
        mCurrentClock = getClockPlugin();
        if (mCurrentClock instanceof DefaultClockController) {
            notifyClockChanged(null);
        mPreviewClocks.reload();
        mListeners.forEach((listener, clocks) -> {
            clocks.reload();
            ClockPlugin clock = clocks.getCurrentClock();
            if (clock instanceof DefaultClockController) {
                listener.onClockChanged(null);
            } else {
            notifyClockChanged(mCurrentClock);
                listener.onClockChanged(clock);
            }
        });
    }

    /**
     * Listener for events that should cause the custom clock face to change.
     */
    public interface ClockChangedListener {
        /**
         * Called when custom clock should change.
         *
         * @param clock Custom clock face to use. A null value indicates the default clock face.
         */
        void onClockChanged(ClockPlugin clock);
    }

    /**
     * Collection of available clocks.
     */
    private final class AvailableClocks implements PluginListener<ClockPlugin> {

        /**
         * Map from expected value stored in settings to plugin for custom clock face.
         */
        private final Map<String, ClockPlugin> mClocks = new ArrayMap<>();

        /**
         * Metadata about available clocks, such as name and preview images.
         */
        private final List<ClockInfo> 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<ClockInfo> 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 (mIsDocked) {
            if (ClockManager.this.isDocked()) {
                final String name = mSettingsWrapper.getDockedClockFace();
                if (name != null) {
                    plugin = mClocks.get(name);
@@ -276,16 +348,5 @@ public final class ClockManager {
            }
            return plugin;
        }

    /**
     * Listener for events that should cause the custom clock face to change.
     */
    public interface ClockChangedListener {
        /**
         * Called when custom clock should change.
         *
         * @param clock Custom clock face to use. A null value indicates the default clock face.
         */
        void onClockChanged(ClockPlugin clock);
    }
}
+40 −3
Original line number Diff line number Diff line
@@ -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<ClockPlugin> 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<ClockPlugin> captor1 = ArgumentCaptor.forClass(ClockPlugin.class);
        ArgumentCaptor<ClockPlugin> 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