Move plugin and settings logic out of KeyguardClockSwitch.

This is in preparation for adding more logic related to docked state.

Bug: 122301289
Test: Used adb to switch between custom clock faces.
Change-Id: I0a13ef4e5894df1280a04c5a9df8bbed8054557b
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 1aff394..7218acf 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -1,15 +1,9 @@
 package com.android.keyguard;
 
-import android.content.ContentResolver;
 import android.content.Context;
-import android.database.ContentObserver;
 import android.graphics.Paint;
 import android.graphics.Paint.Style;
-import android.os.Handler;
-import android.os.Looper;
-import android.provider.Settings;
 import android.util.AttributeSet;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -18,29 +12,19 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.keyguard.clock.BubbleClockController;
-import com.android.keyguard.clock.StretchAnalogClockController;
-import com.android.keyguard.clock.TypeClockController;
+import com.android.keyguard.clock.ClockManager;
 import com.android.systemui.Dependency;
 import com.android.systemui.plugins.ClockPlugin;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.policy.ExtensionController;
-import com.android.systemui.statusbar.policy.ExtensionController.Extension;
 
-import java.util.Objects;
 import java.util.TimeZone;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
 
 /**
  * Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
  */
 public class KeyguardClockSwitch extends RelativeLayout {
 
-    private LayoutInflater mLayoutInflater;
-
-    private final ContentResolver mContentResolver;
     /**
      * Optional/alternative clock injected via plugin.
      */
@@ -63,14 +47,6 @@
      */
     private View mKeyguardStatusArea;
     /**
-     * Used to select between plugin or default implementations of ClockPlugin interface.
-     */
-    private Extension<ClockPlugin> mClockExtension;
-    /**
-     * Consumer that accepts the a new ClockPlugin implementation when the Extension reloads.
-     */
-    private final Consumer<ClockPlugin> mClockPluginConsumer = plugin -> setClockPlugin(plugin);
-    /**
      * Maintain state so that a newly connected plugin can be initialized.
      */
     private float mDarkAmount;
@@ -94,16 +70,7 @@
                 }
     };
 
-    private final ContentObserver mContentObserver =
-            new ContentObserver(new Handler(Looper.getMainLooper())) {
-                @Override
-                public void onChange(boolean selfChange) {
-                    super.onChange(selfChange);
-                    if (mClockExtension != null) {
-                        mClockExtension.reload();
-                    }
-                }
-    };
+    private ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin;
 
     public KeyguardClockSwitch(Context context) {
         this(context, null);
@@ -111,8 +78,6 @@
 
     public KeyguardClockSwitch(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mLayoutInflater = LayoutInflater.from(context);
-        mContentResolver = context.getContentResolver();
     }
 
     /**
@@ -133,45 +98,14 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mClockExtension = Dependency.get(ExtensionController.class).newExtension(ClockPlugin.class)
-                .withPlugin(ClockPlugin.class)
-                .withCallback(mClockPluginConsumer)
-                // Using withDefault even though this isn't the default as a workaround.
-                // ExtensionBulider doesn't provide the ability to supply a ClockPlugin
-                // instance based off of the value of a setting. Since multiple "default"
-                // can be provided, using a supplier that changes the settings value.
-                // A null return will cause Extension#reload to look at the next "default"
-                // supplier.
-                .withDefault(
-                        new SettingsGattedSupplier(
-                                mContentResolver,
-                                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
-                                BubbleClockController.class.getName(),
-                                () -> BubbleClockController.build(mLayoutInflater)))
-                .withDefault(
-                        new SettingsGattedSupplier(
-                                mContentResolver,
-                                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
-                                StretchAnalogClockController.class.getName(),
-                                () -> StretchAnalogClockController.build(mLayoutInflater)))
-                .withDefault(
-                        new SettingsGattedSupplier(
-                                mContentResolver,
-                                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
-                                TypeClockController.class.getName(),
-                                () -> TypeClockController.build(mLayoutInflater)))
-                .build();
-        mContentResolver.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
-                false, mContentObserver);
+        Dependency.get(ClockManager.class).addOnClockChangedListener(mClockChangedListener);
         Dependency.get(StatusBarStateController.class).addCallback(mStateListener);
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mClockExtension.destroy();
-        mContentResolver.unregisterContentObserver(mContentObserver);
+        Dependency.get(ClockManager.class).removeOnClockChangedListener(mClockChangedListener);
         Dependency.get(StatusBarStateController.class).removeCallback(mStateListener);
     }
 
@@ -313,52 +247,12 @@
     }
 
     @VisibleForTesting (otherwise = VisibleForTesting.NONE)
-    Consumer<ClockPlugin> getClockPluginConsumer() {
-        return mClockPluginConsumer;
+    ClockManager.ClockChangedListener getClockChangedListener() {
+        return mClockChangedListener;
     }
 
     @VisibleForTesting (otherwise = VisibleForTesting.NONE)
     StatusBarStateController.StateListener getStateListener() {
         return mStateListener;
     }
-
-    /**
-     * Supplier that only gets an instance when a settings value matches expected value.
-     */
-    private static class SettingsGattedSupplier implements Supplier<ClockPlugin> {
-
-        private final ContentResolver mContentResolver;
-        private final String mKey;
-        private final String mValue;
-        private final Supplier<ClockPlugin> mSupplier;
-
-        /**
-         * Constructs a supplier that changes secure setting key against value.
-         *
-         * @param contentResolver Used to look up settings value.
-         * @param key Settings key.
-         * @param value If the setting matches this values that get supplies a ClockPlugin
-         *        instance.
-         * @param supplier Supplier of ClockPlugin instance, only used if the setting
-         *        matches value.
-         */
-        SettingsGattedSupplier(ContentResolver contentResolver, String key, String value,
-                Supplier<ClockPlugin> supplier) {
-            mContentResolver = contentResolver;
-            mKey = key;
-            mValue = value;
-            mSupplier = supplier;
-        }
-
-        /**
-         * Returns null if the settings value doesn't match the expected value.
-         *
-         * A null return causes Extension#reload to skip this supplier and move to the next.
-         */
-        @Override
-        public ClockPlugin get() {
-            final String currentValue = Settings.Secure.getString(mContentResolver, mKey);
-            return Objects.equals(currentValue, mValue) ? mSupplier.get() : null;
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
new file mode 100644
index 0000000..3217ca6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
@@ -0,0 +1,202 @@
+/*
+ * 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.keyguard.clock;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.view.LayoutInflater;
+
+import com.android.systemui.plugins.ClockPlugin;
+import com.android.systemui.statusbar.policy.ExtensionController;
+import com.android.systemui.statusbar.policy.ExtensionController.Extension;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Manages custom clock faces.
+ */
+@Singleton
+public final class ClockManager {
+
+    private final LayoutInflater mLayoutInflater;
+    private final ContentResolver mContentResolver;
+
+    /**
+     * Observe settings changes to know when to switch the clock face.
+     */
+    private final ContentObserver mContentObserver =
+            new ContentObserver(new Handler(Looper.getMainLooper())) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    super.onChange(selfChange);
+                    if (mClockExtension != null) {
+                        mClockExtension.reload();
+                    }
+                }
+            };
+
+    private final ExtensionController mExtensionController;
+    /**
+     * Used to select between plugin or default implementations of ClockPlugin interface.
+     */
+    private Extension<ClockPlugin> mClockExtension;
+    /**
+     * Consumer that accepts the a new ClockPlugin implementation when the Extension reloads.
+     */
+    private final Consumer<ClockPlugin> mClockPluginConsumer = this::setClockPlugin;
+
+    private final List<ClockChangedListener> mListeners = new ArrayList<>();
+
+    @Inject
+    public ClockManager(Context context, ExtensionController extensionController) {
+        mExtensionController = extensionController;
+        mLayoutInflater = LayoutInflater.from(context);
+        mContentResolver = context.getContentResolver();
+    }
+
+    /**
+     * Add listener to be notified when clock implementation should change.
+     */
+    public void addOnClockChangedListener(ClockChangedListener listener) {
+        if (mListeners.isEmpty()) {
+            register();
+        }
+        mListeners.add(listener);
+        if (mClockExtension != null) {
+            mClockExtension.reload();
+        }
+    }
+
+    /**
+     * Remove listener added with {@link addOnClockChangedListener}.
+     */
+    public void removeOnClockChangedListener(ClockChangedListener listener) {
+        mListeners.remove(listener);
+        if (mListeners.isEmpty()) {
+            unregister();
+        }
+    }
+
+    private void setClockPlugin(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 register() {
+        mContentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
+                false, mContentObserver);
+        mClockExtension = mExtensionController.newExtension(ClockPlugin.class)
+            .withPlugin(ClockPlugin.class)
+            .withCallback(mClockPluginConsumer)
+            // Using withDefault even though this isn't the default as a workaround.
+            // ExtensionBuilder doesn't provide the ability to supply a ClockPlugin
+            // instance based off of the value of a setting. Since multiple "default"
+            // can be provided, using a supplier that changes the settings value.
+            // A null return will cause Extension#reload to look at the next "default"
+            // supplier.
+            .withDefault(
+                    new SettingsGattedSupplier(
+                        mContentResolver,
+                        Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
+                        BubbleClockController.class.getName(),
+                            () -> BubbleClockController.build(mLayoutInflater)))
+            .withDefault(
+                    new SettingsGattedSupplier(
+                        mContentResolver,
+                        Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
+                        StretchAnalogClockController.class.getName(),
+                            () -> StretchAnalogClockController.build(mLayoutInflater)))
+            .withDefault(
+                    new SettingsGattedSupplier(
+                        mContentResolver,
+                        Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
+                        TypeClockController.class.getName(),
+                            () -> TypeClockController.build(mLayoutInflater)))
+            .build();
+    }
+
+    private void unregister() {
+        mContentResolver.unregisterContentObserver(mContentObserver);
+        mClockExtension.destroy();
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Supplier that only gets an instance when a settings value matches expected value.
+     */
+    private static class SettingsGattedSupplier implements Supplier<ClockPlugin> {
+
+        private final ContentResolver mContentResolver;
+        private final String mKey;
+        private final String mValue;
+        private final Supplier<ClockPlugin> mSupplier;
+
+        /**
+         * Constructs a supplier that changes secure setting key against value.
+         *
+         * @param contentResolver Used to look up settings value.
+         * @param key Settings key.
+         * @param value If the setting matches this values that get supplies a ClockPlugin
+         *        instance.
+         * @param supplier Supplier of ClockPlugin instance, only used if the setting
+         *        matches value.
+         */
+        SettingsGattedSupplier(ContentResolver contentResolver, String key, String value,
+                Supplier<ClockPlugin> supplier) {
+            mContentResolver = contentResolver;
+            mKey = key;
+            mValue = value;
+            mSupplier = supplier;
+        }
+
+        /**
+         * Returns null if the settings value doesn't match the expected value.
+         *
+         * A null return causes Extension#reload to skip this supplier and move to the next.
+         */
+        @Override
+        public ClockPlugin get() {
+            final String currentValue = Settings.Secure.getString(mContentResolver, mKey);
+            return Objects.equals(currentValue, mValue) ? mSupplier.get() : null;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index ec6ecc6..d99f234 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -29,6 +29,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.Preconditions;
+import com.android.keyguard.clock.ClockManager;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.systemui.appops.AppOpsController;
 import com.android.systemui.assist.AssistManager;
@@ -283,6 +284,7 @@
     @Inject @Named(TIME_TICK_HANDLER_NAME) Lazy<Handler> mTimeTickHandler;
     @Nullable
     @Inject @Named(LEAK_REPORT_EMAIL_NAME) Lazy<String> mLeakReportEmail;
+    @Inject Lazy<ClockManager> mClockManager;
 
     @Inject
     public Dependency() {
@@ -449,6 +451,7 @@
         mProviders.put(NotificationAlertingManager.class, mNotificationAlertingManager::get);
         mProviders.put(ForegroundServiceNotificationListener.class,
                 mForegroundServiceNotificationListener::get);
+        mProviders.put(ClockManager.class, mClockManager::get);
 
         // TODO(b/118592525): to support multi-display , we start to add something which is
         //                    per-display, while others may be global. I think it's time to add
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index fbc1c20..d80b444 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -39,6 +39,7 @@
 import android.widget.FrameLayout;
 import android.widget.TextClock;
 
+import com.android.keyguard.clock.ClockManager;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ClockPlugin;
 import com.android.systemui.statusbar.StatusBarState;
@@ -51,8 +52,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.function.Consumer;
-
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 // Need to run on the main thread because KeyguardSliceView$Row init checks for
@@ -85,7 +84,7 @@
         TextClock pluginView = new TextClock(getContext());
         when(plugin.getView()).thenReturn(pluginView);
 
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
 
         verify(mClockView).setVisibility(GONE);
         assertThat(plugin.getView().getParent()).isEqualTo(mClockContainer);
@@ -102,7 +101,7 @@
         TextClock pluginView = new TextClock(getContext());
         when(plugin.getBigClockView()).thenReturn(pluginView);
         // WHEN the plugin is connected
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
         // THEN the big clock container is visible and it is the parent of the
         // big clock view.
         assertThat(bigClockContainer.getVisibility()).isEqualTo(VISIBLE);
@@ -112,7 +111,7 @@
     @Test
     public void onPluginConnected_nullView() {
         ClockPlugin plugin = mock(ClockPlugin.class);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
         verify(mClockView, never()).setVisibility(GONE);
     }
 
@@ -121,11 +120,11 @@
         // GIVEN a plugin has already connected
         ClockPlugin plugin1 = mock(ClockPlugin.class);
         when(plugin1.getView()).thenReturn(new TextClock(getContext()));
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin1);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin1);
         // WHEN a second plugin is connected
         ClockPlugin plugin2 = mock(ClockPlugin.class);
         when(plugin2.getView()).thenReturn(new TextClock(getContext()));
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin2);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin2);
         // THEN only the view from the second plugin should be a child of KeyguardClockSwitch.
         assertThat(plugin2.getView().getParent()).isEqualTo(mClockContainer);
         assertThat(plugin1.getView().getParent()).isNull();
@@ -137,7 +136,7 @@
         mKeyguardClockSwitch.setDarkAmount(0.5f);
         // WHEN a plugin is connected
         ClockPlugin plugin = mock(ClockPlugin.class);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
         // THEN dark amount should be initalized on the plugin.
         verify(plugin).setDarkAmount(0.5f);
     }
@@ -149,8 +148,8 @@
         when(plugin.getView()).thenReturn(pluginView);
         mClockView.setVisibility(GONE);
 
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(null);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(null);
 
         verify(mClockView).setVisibility(VISIBLE);
         assertThat(plugin.getView().getParent()).isNull();
@@ -167,8 +166,8 @@
         TextClock pluginView = new TextClock(getContext());
         when(plugin.getBigClockView()).thenReturn(pluginView);
         // WHEN the plugin is connected and then disconnected
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(null);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(null);
         // THEN the big lock container is GONE and the big clock view doesn't have
         // a parent.
         assertThat(bigClockContainer.getVisibility()).isEqualTo(GONE);
@@ -178,8 +177,8 @@
     @Test
     public void onPluginDisconnected_nullView() {
         ClockPlugin plugin = mock(ClockPlugin.class);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(null);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(null);
         verify(mClockView, never()).setVisibility(GONE);
     }
 
@@ -188,13 +187,13 @@
         // GIVEN two plugins are connected
         ClockPlugin plugin1 = mock(ClockPlugin.class);
         when(plugin1.getView()).thenReturn(new TextClock(getContext()));
-        Consumer<ClockPlugin> consumer = mKeyguardClockSwitch.getClockPluginConsumer();
-        consumer.accept(plugin1);
+        ClockManager.ClockChangedListener listener = mKeyguardClockSwitch.getClockChangedListener();
+        listener.onClockChanged(plugin1);
         ClockPlugin plugin2 = mock(ClockPlugin.class);
         when(plugin2.getView()).thenReturn(new TextClock(getContext()));
-        consumer.accept(plugin2);
+        listener.onClockChanged(plugin2);
         // WHEN the second plugin is disconnected
-        consumer.accept(null);
+        listener.onClockChanged(null);
         // THEN the default clock should be shown.
         verify(mClockView).setVisibility(VISIBLE);
         assertThat(plugin1.getView().getParent()).isNull();
@@ -213,7 +212,7 @@
         ClockPlugin plugin = mock(ClockPlugin.class);
         TextClock pluginView = new TextClock(getContext());
         when(plugin.getView()).thenReturn(pluginView);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
 
         mKeyguardClockSwitch.setTextColor(Color.WHITE);
 
@@ -237,7 +236,7 @@
         TextClock pluginView = new TextClock(getContext());
         when(plugin.getView()).thenReturn(pluginView);
         Style style = mock(Style.class);
-        mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin);
+        mKeyguardClockSwitch.getClockChangedListener().onClockChanged(plugin);
 
         mKeyguardClockSwitch.setStyle(style);