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

Commit d8445eae authored by Robert Snoeberger's avatar Robert Snoeberger
Browse files

ClockManager listeners receive separate ClockPlugin instances.

Fixes: 129133354
Test: atest ActivityManagerDisplayKeyguardTests.java with custom clock.
Change-Id: Ic7447c1bbf2a66d82f2b11062e5ca4a5de846a4e
parent 60a54acb
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