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

Commit 15b4af19 authored by Robert Snoeberger's avatar Robert Snoeberger
Browse files

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
parent f9c42c76
Loading
Loading
Loading
Loading
+6 −112
Original line number Diff line number Diff line
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 android.widget.TextClock;

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.
     */
@@ -62,14 +46,6 @@ public class KeyguardClockSwitch extends RelativeLayout {
     * or not to show it below the alternate clock.
     */
    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.
     */
@@ -94,16 +70,7 @@ public class KeyguardClockSwitch extends RelativeLayout {
                }
    };

    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 class KeyguardClockSwitch extends RelativeLayout {

    public KeyguardClockSwitch(Context context, AttributeSet attrs) {
        super(context, attrs);
        mLayoutInflater = LayoutInflater.from(context);
        mContentResolver = context.getContentResolver();
    }

    /**
@@ -133,45 +98,14 @@ public class KeyguardClockSwitch extends RelativeLayout {
    @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 @@ public class KeyguardClockSwitch extends RelativeLayout {
    }

    @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;
        }
    }
}
+202 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import com.android.internal.app.ColorDisplayController;
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 @@ public class Dependency extends SystemUI {
    @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 @@ public class Dependency extends SystemUI {
        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
+19 −20
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import android.view.ViewGroup;
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.InjectMocks;
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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
    @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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        // 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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
    @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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        // 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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        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 @@ public class KeyguardClockSwitchTest extends SysuiTestCase {
        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);