| /* |
| * 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.content.res.Resources; |
| import android.database.ContentObserver; |
| import android.graphics.BitmapFactory; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.provider.Settings; |
| import android.view.LayoutInflater; |
| |
| import com.android.keyguard.R; |
| 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; |
| |
| private final List<ClockInfo> mClockInfos = new ArrayList<>(); |
| /** |
| * 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(); |
| |
| Resources res = context.getResources(); |
| mClockInfos.add(ClockInfo.builder() |
| .setName("default") |
| .setTitle(res.getString(R.string.clock_title_default)) |
| .setId("default") |
| .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.default_thumbnail)) |
| .setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.default_preview)) |
| .build()); |
| mClockInfos.add(ClockInfo.builder() |
| .setName("bubble") |
| .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)) |
| .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)) |
| .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)) |
| .build()); |
| } |
| |
| /** |
| * 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(); |
| } |
| } |
| |
| /** |
| * Get information about available clock faces. |
| */ |
| List<ClockInfo> getClockInfos() { |
| return mClockInfos; |
| } |
| |
| 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; |
| } |
| } |
| } |