blob: 294c725af481e713659d612cf5047d11c20e6a1e [file] [log] [blame]
/*
* 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.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.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.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import androidx.annotation.VisibleForTesting;
import com.android.internal.colorextraction.ColorExtractor;
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.plugins.PluginListener;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.util.InjectionInflationController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Manages custom clock faces for AOD and lock screen.
*/
@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 ContentResolver mContentResolver;
private final SettingsWrapper mSettingsWrapper;
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
/**
* Observe settings changes to know when to switch the clock face.
*/
private final ContentObserver mContentObserver =
new ContentObserver(mMainHandler) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
reload();
}
};
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;
/**
* Observe changes to dock state to know when to switch the clock face.
*/
private final DockEventListener mDockEventListener =
new DockEventListener() {
@Override
public void onEvent(int event) {
mIsDocked = (event == DockManager.STATE_DOCKED
|| event == DockManager.STATE_DOCKED_HIDE);
reload();
}
};
@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, InjectionInflationController injectionInflater,
PluginManager pluginManager, @Nullable DockManager dockManager,
SysuiColorExtractor colorExtractor) {
this(context, injectionInflater, pluginManager, dockManager, colorExtractor,
context.getContentResolver(), new SettingsWrapper(context.getContentResolver()));
}
ClockManager(Context context, InjectionInflationController injectionInflater,
PluginManager pluginManager, @Nullable DockManager dockManager,
SysuiColorExtractor colorExtractor, ContentResolver contentResolver,
SettingsWrapper settingsWrapper) {
mPluginManager = pluginManager;
mDockManager = dockManager;
mColorExtractor = colorExtractor;
mContentResolver = contentResolver;
mSettingsWrapper = settingsWrapper;
Resources res = context.getResources();
LayoutInflater layoutInflater = injectionInflater.injectable(LayoutInflater.from(context));
addClockPlugin(new DefaultClockController(res, layoutInflater));
addClockPlugin(new BubbleClockController(res, layoutInflater));
addClockPlugin(new StretchAnalogClockController(res, layoutInflater));
addClockPlugin(new TypeClockController(res, layoutInflater));
// Store the size of the display for generation of clock preview.
DisplayMetrics dm = res.getDisplayMetrics();
mWidth = dm.widthPixels;
mHeight = dm.heightPixels;
}
/**
* Add listener to be notified when clock implementation should change.
*/
public void addOnClockChangedListener(ClockChangedListener listener) {
if (mListeners.isEmpty()) {
register();
}
mListeners.add(listener);
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;
}
/**
* 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;
}
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(() -> getClockPreview(id))
.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;
}
}
}
/**
* 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) {
FutureTask<Bitmap> task = new FutureTask<>(new Callable<Bitmap>() {
@Override
public Bitmap call() {
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888);
ClockPlugin plugin = mClocks.get(clockId);
if (plugin == null) {
return null;
}
// 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.onTimeTick();
// Draw clock view hierarchy to canvas.
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
dispatchVisibilityAggregated(clockView, true);
clockView.measure(MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
clockView.layout(0, 0, mWidth, mHeight);
clockView.draw(canvas);
return bitmap;
}
});
if (Looper.myLooper() == Looper.getMainLooper()) {
task.run();
} else {
mMainHandler.post(task);
}
try {
return task.get();
} catch (Exception e) {
Log.e(TAG, "Error completing task", e);
return null;
}
}
private void dispatchVisibilityAggregated(View view, boolean isVisible) {
// Similar to View.dispatchVisibilityAggregated implementation.
final boolean thisVisible = view.getVisibility() == View.VISIBLE;
if (thisVisible || !isVisible) {
view.onVisibilityAggregated(isVisible);
}
if (view instanceof ViewGroup) {
isVisible = thisVisible && isVisible;
ViewGroup vg = (ViewGroup) view;
int count = vg.getChildCount();
for (int i = 0; i < count; i++) {
dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
}
}
}
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 register() {
mPluginManager.addPluginListener(mClockPluginListener, ClockPlugin.class, true);
mContentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
false, mContentObserver);
mContentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE),
false, mContentObserver);
if (mDockManager != null) {
mDockManager.addListener(mDockEventListener);
}
}
private void unregister() {
mPluginManager.removePluginListener(mClockPluginListener);
mContentResolver.unregisterContentObserver(mContentObserver);
if (mDockManager != null) {
mDockManager.removeListener(mDockEventListener);
}
}
private void reload() {
mCurrentClock = getClockPlugin();
if (mCurrentClock instanceof DefaultClockController) {
notifyClockChanged(null);
} else {
notifyClockChanged(mCurrentClock);
}
}
private ClockPlugin getClockPlugin() {
ClockPlugin plugin = null;
if (mIsDocked) {
final String name = mSettingsWrapper.getDockedClockFace();
if (name != null) {
plugin = mClocks.get(name);
if (plugin != null) {
return plugin;
}
}
}
final String name = mSettingsWrapper.getLockScreenCustomClockFace();
if (name != null) {
plugin = mClocks.get(name);
}
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);
}
}