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

Commit 71e50791 authored by Robert Snoeberger's avatar Robert Snoeberger
Browse files

Generate realistic preview images of custom clock faces.

Bug: 123704608
Test: atest ClockManagerTest
Change-Id: I2dcc50120995f99ed170cc01a934d1db728f5963
parent 78e3b2da
Loading
Loading
Loading
Loading
−7.5 KiB
Loading image diff...
−6.6 KiB
Loading image diff...
−20 KiB
Loading image diff...
+151 −51
Original line number Diff line number Diff line
@@ -16,41 +16,58 @@
package com.android.keyguard.clock;

import android.annotation.Nullable;
import android.app.WallpaperManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;

import androidx.annotation.VisibleForTesting;

import com.android.internal.colorextraction.ColorExtractor;
import com.android.keyguard.R;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManager.DockEventListener;
import com.android.systemui.plugins.ClockPlugin;
import com.android.systemui.statusbar.policy.ExtensionController;
import com.android.systemui.statusbar.policy.ExtensionController.Extension;
import com.android.systemui.util.InjectionInflationController;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.Map;
import java.util.function.Supplier;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Manages custom clock faces.
 * Manages custom clock faces for AOD and lock screen.
 */
@Singleton
public final class ClockManager {

    private final ContentResolver mContentResolver;

    private final List<ClockInfo> mClockInfos = new ArrayList<>();
    /**
     * Map from expected value stored in settings to supplier of custom clock face.
     */
    private final Map<String, Supplier<ClockPlugin>> mClocks = new ArrayMap<>();
    @Nullable private ClockPlugin mCurrentClock;

    private final ContentResolver mContentResolver;
    private final SettingsWrapper mSettingsWrapper;
    /**
     * Observe settings changes to know when to switch the clock face.
     */
@@ -59,24 +76,9 @@ public final class ClockManager {
                @Override
                public void onChange(boolean selfChange) {
                    super.onChange(selfChange);
                    if (mClockExtension != null) {
                        mClockExtension.reload();
                    }
                    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;
    /**
     * Supplier of default ClockPlugin implementation.
     */
    private final DefaultClockSupplier mDefaultClockSupplier;
    /**
     * Observe changes to dock state to know when to switch the clock face.
     */
@@ -84,25 +86,38 @@ public final class ClockManager {
            new DockEventListener() {
                @Override
                public void onEvent(int event) {
                    final boolean isDocked = (event == DockManager.STATE_DOCKED
                    mIsDocked = (event == DockManager.STATE_DOCKED
                            || event == DockManager.STATE_DOCKED_HIDE);
                    mDefaultClockSupplier.setDocked(isDocked);
                    if (mClockExtension != null) {
                        mClockExtension.reload();
                    }
                    reload();
                }
            };
    @Nullable
    private final DockManager mDockManager;
    @Nullable private final 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<>();

    private final SysuiColorExtractor mColorExtractor;
    private final int mWidth;
    private final int mHeight;

    @Inject
    public ClockManager(Context context, ExtensionController extensionController,
            @Nullable DockManager dockManager) {
        mExtensionController = extensionController;
    public ClockManager(Context context, InjectionInflationController injectionInflater,
            @Nullable DockManager dockManager, SysuiColorExtractor colorExtractor) {
        this(context, injectionInflater, dockManager, colorExtractor, context.getContentResolver(),
                new SettingsWrapper(context.getContentResolver()));
    }

    ClockManager(Context context, InjectionInflationController injectionInflater,
            @Nullable DockManager dockManager, SysuiColorExtractor colorExtractor,
            ContentResolver contentResolver, SettingsWrapper settingsWrapper) {
        mDockManager = dockManager;
        mContentResolver = context.getContentResolver();
        mColorExtractor = colorExtractor;
        mContentResolver = contentResolver;
        mSettingsWrapper = settingsWrapper;

        Resources res = context.getResources();
        mClockInfos.add(ClockInfo.builder()
@@ -117,25 +132,35 @@ public final class ClockManager {
                .setTitle(res.getString(R.string.clock_title_bubble))
                .setId(BubbleClockController.class.getName())
                .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.bubble_thumbnail))
                .setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.bubble_preview))
                .setPreview(() -> getClockPreview(BubbleClockController.class.getName()))
                .build());
        mClockInfos.add(ClockInfo.builder()
                .setName("stretch")
                .setTitle(res.getString(R.string.clock_title_stretch))
                .setId(StretchAnalogClockController.class.getName())
                .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.stretch_thumbnail))
                .setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.stretch_preview))
                .setPreview(() -> getClockPreview(StretchAnalogClockController.class.getName()))
                .build());
        mClockInfos.add(ClockInfo.builder()
                .setName("type")
                .setTitle(res.getString(R.string.clock_title_type))
                .setId(TypeClockController.class.getName())
                .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.type_thumbnail))
                .setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.type_preview))
                .setPreview(() -> getClockPreview(TypeClockController.class.getName()))
                .build());

        mDefaultClockSupplier = new DefaultClockSupplier(new SettingsWrapper(mContentResolver),
                LayoutInflater.from(context));
        LayoutInflater layoutInflater = injectionInflater.injectable(LayoutInflater.from(context));
        mClocks.put(BubbleClockController.class.getName(),
                () -> BubbleClockController.build(layoutInflater));
        mClocks.put(StretchAnalogClockController.class.getName(),
                () -> StretchAnalogClockController.build(layoutInflater));
        mClocks.put(TypeClockController.class.getName(),
                () -> TypeClockController.build(layoutInflater));

        // Store the size of the display for generation of clock preview.
        DisplayMetrics dm = res.getDisplayMetrics();
        mWidth = dm.widthPixels;
        mHeight = dm.heightPixels;
    }

    /**
@@ -146,9 +171,7 @@ public final class ClockManager {
            register();
        }
        mListeners.add(listener);
        if (mClockExtension != null) {
            mClockExtension.reload();
        }
        reload();
    }

    /**
@@ -168,7 +191,66 @@ public final class ClockManager {
        return mClockInfos;
    }

    private void setClockPlugin(ClockPlugin plugin) {
    /**
     * Get the current clock.
     * @returns current custom clock or null for default.
     */
    @Nullable
    ClockPlugin getCurrentClock() {
        return mCurrentClock;
    }

    @VisibleForTesting
    boolean isDocked() {
        return mIsDocked;
    }

    @VisibleForTesting
    ContentObserver getContentObserver() {
        return mContentObserver;
    }

    /**
     * Generate a realistic preview of a clock face.
     * @param clockId ID of clock to use for preview, should be obtained from {@link getClockInfos}.
     *        Returns null if clockId is not found.
     */
    @Nullable
    private Bitmap getClockPreview(String clockId) {
        Supplier<ClockPlugin> supplier = mClocks.get(clockId);
        if (supplier == null) {
            return null;
        }
        ClockPlugin plugin = supplier.get();

        // Use the big clock view for the preview
        View clockView = plugin.getBigClockView();
        if (clockView == null) {
            return null;
        }

        // Initialize state of plugin before generating preview.
        plugin.setDarkAmount(1f);
        plugin.setTextColor(Color.WHITE);

        ColorExtractor.GradientColors colors = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK,
                true);
        plugin.setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
        plugin.dozeTimeTick();

        // Draw clock view hierarchy to canvas.
        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        clockView.measure(MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
        clockView.layout(0, 0, mWidth, mHeight);
        canvas.drawColor(Color.BLACK);
        clockView.draw(canvas);

        return bitmap;
    }

    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.
@@ -186,11 +268,6 @@ public final class ClockManager {
        if (mDockManager != null) {
            mDockManager.addListener(mDockEventListener);
        }
        mClockExtension = mExtensionController.newExtension(ClockPlugin.class)
            .withPlugin(ClockPlugin.class)
            .withCallback(mClockPluginConsumer)
            .withDefault(mDefaultClockSupplier)
            .build();
    }

    private void unregister() {
@@ -198,12 +275,35 @@ public final class ClockManager {
        if (mDockManager != null) {
            mDockManager.removeListener(mDockEventListener);
        }
        mClockExtension.destroy();
    }

    @VisibleForTesting
    boolean isDocked() {
        return mDefaultClockSupplier.isDocked();
    private void reload() {
        mCurrentClock = getClockPlugin();
        notifyClockChanged(mCurrentClock);
    }

    private ClockPlugin getClockPlugin() {
        ClockPlugin plugin = null;
        if (mIsDocked) {
            final String name = mSettingsWrapper.getDockedClockFace();
            if (name != null) {
                Supplier<ClockPlugin> supplier = mClocks.get(name);
                if (supplier != null) {
                    plugin = supplier.get();
                    if (plugin != null) {
                        return plugin;
                    }
                }
            }
        }
        final String name = mSettingsWrapper.getLockScreenCustomClockFace();
        if (name != null) {
            Supplier<ClockPlugin> supplier = mClocks.get(name);
            if (supplier != null) {
                plugin = supplier.get();
            }
        }
        return plugin;
    }

    /**
+0 −101
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.util.ArrayMap;
import android.view.LayoutInflater;

import com.android.systemui.plugins.ClockPlugin;

import java.util.Map;
import java.util.function.Supplier;

/**
 * Supplier that only gets an instance when a settings value matches expected value.
 */
public class DefaultClockSupplier implements Supplier<ClockPlugin> {

    private final SettingsWrapper mSettingsWrapper;
    /**
     * Map from expected value stored in settings to supplier of custom clock face.
     */
    private final Map<String, Supplier<ClockPlugin>> mClocks = new ArrayMap<>();
    /**
     * When docked, the DOCKED_CLOCK_FACE setting will be checked for the custom clock face
     * to show.
     */
    private boolean mIsDocked;

    /**
     * Constructs a supplier that changes secure setting key against value.
     *
     * @param settingsWrapper Wrapper around settings used to look up the custom clock face.
     * @param layoutInflater Provided to clocks as dependency to inflate clock views.
     */
    public DefaultClockSupplier(SettingsWrapper settingsWrapper, LayoutInflater layoutInflater) {
        mSettingsWrapper = settingsWrapper;

        mClocks.put(BubbleClockController.class.getName(),
                () -> BubbleClockController.build(layoutInflater));
        mClocks.put(StretchAnalogClockController.class.getName(),
                () -> StretchAnalogClockController.build(layoutInflater));
        mClocks.put(TypeClockController.class.getName(),
                () -> TypeClockController.build(layoutInflater));
    }

    /**
     * Sets the dock state.
     *
     * @param isDocked True when docked, false otherwise.
     */
    public void setDocked(boolean isDocked) {
        mIsDocked = isDocked;
    }

    boolean isDocked() {
        return mIsDocked;
    }

    /**
     * Get the custom clock face based on values in settings.
     *
     * @return Custom clock face, null if the settings value doesn't match a custom clock.
     */
    @Override
    public ClockPlugin get() {
        ClockPlugin plugin = null;
        if (mIsDocked) {
            final String name = mSettingsWrapper.getDockedClockFace();
            if (name != null) {
                Supplier<ClockPlugin> supplier = mClocks.get(name);
                if (supplier != null) {
                    plugin = supplier.get();
                    if (plugin != null) {
                        return plugin;
                    }
                }
            }
        }
        final String name = mSettingsWrapper.getLockScreenCustomClockFace();
        if (name != null) {
            Supplier<ClockPlugin> supplier = mClocks.get(name);
            if (supplier != null) {
                plugin = supplier.get();
            }
        }
        return plugin;
    }
}
Loading