blob: 294c725af481e713659d612cf5047d11c20e6a1e [file] [log] [blame]
Robert Snoeberger15b4af12019-01-18 15:37:27 -05001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.keyguard.clock;
17
Robert Snoebergerb300a4e2019-02-13 20:13:53 +000018import android.annotation.Nullable;
Robert Snoeberger71e50792019-02-15 15:48:01 -050019import android.app.WallpaperManager;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050020import android.content.ContentResolver;
21import android.content.Context;
Robert Snoeberger6b244b02019-02-04 15:33:31 -050022import android.content.res.Resources;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050023import android.database.ContentObserver;
Robert Snoeberger71e50792019-02-15 15:48:01 -050024import android.graphics.Bitmap;
25import android.graphics.Bitmap.Config;
Robert Snoeberger71e50792019-02-15 15:48:01 -050026import android.graphics.Canvas;
27import android.graphics.Color;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050028import android.os.Handler;
29import android.os.Looper;
30import android.provider.Settings;
Robert Snoeberger71e50792019-02-15 15:48:01 -050031import android.util.ArrayMap;
32import android.util.DisplayMetrics;
Robert Snoeberger9ad03f42019-02-28 14:47:49 -050033import android.util.Log;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050034import android.view.LayoutInflater;
Robert Snoeberger71e50792019-02-15 15:48:01 -050035import android.view.View;
36import android.view.View.MeasureSpec;
Robert Snoeberger60cbc962019-02-20 15:46:43 -050037import android.view.ViewGroup;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050038
Robert Snoebergerb300a4e2019-02-13 20:13:53 +000039import androidx.annotation.VisibleForTesting;
40
Robert Snoeberger71e50792019-02-15 15:48:01 -050041import com.android.internal.colorextraction.ColorExtractor;
Robert Snoeberger71e50792019-02-15 15:48:01 -050042import com.android.systemui.colorextraction.SysuiColorExtractor;
Robert Snoebergerb300a4e2019-02-13 20:13:53 +000043import com.android.systemui.dock.DockManager;
44import com.android.systemui.dock.DockManager.DockEventListener;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050045import com.android.systemui.plugins.ClockPlugin;
Robert Snoeberger9ad03f42019-02-28 14:47:49 -050046import com.android.systemui.plugins.PluginListener;
47import com.android.systemui.shared.plugins.PluginManager;
Robert Snoeberger71e50792019-02-15 15:48:01 -050048import com.android.systemui.util.InjectionInflationController;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050049
50import java.util.ArrayList;
51import java.util.List;
Robert Snoeberger71e50792019-02-15 15:48:01 -050052import java.util.Map;
Robert Snoeberger9ad03f42019-02-28 14:47:49 -050053import java.util.concurrent.Callable;
54import java.util.concurrent.FutureTask;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050055
56import javax.inject.Inject;
57import javax.inject.Singleton;
58
59/**
Robert Snoeberger71e50792019-02-15 15:48:01 -050060 * Manages custom clock faces for AOD and lock screen.
Robert Snoeberger15b4af12019-01-18 15:37:27 -050061 */
62@Singleton
63public final class ClockManager {
64
Robert Snoeberger00257512019-02-27 16:44:04 -050065 private static final String TAG = "ClockOptsProvider";
66 private static final String DEFAULT_CLOCK_ID = "default";
67
Robert Snoeberger6b244b02019-02-04 15:33:31 -050068 private final List<ClockInfo> mClockInfos = new ArrayList<>();
Robert Snoeberger15b4af12019-01-18 15:37:27 -050069 /**
Robert Snoeberger71e50792019-02-15 15:48:01 -050070 * Map from expected value stored in settings to supplier of custom clock face.
71 */
Robert Snoeberger9ad03f42019-02-28 14:47:49 -050072 private final Map<String, ClockPlugin> mClocks = new ArrayMap<>();
Robert Snoeberger71e50792019-02-15 15:48:01 -050073 @Nullable private ClockPlugin mCurrentClock;
74
75 private final ContentResolver mContentResolver;
76 private final SettingsWrapper mSettingsWrapper;
Robert Snoeberger9ad03f42019-02-28 14:47:49 -050077 private final Handler mMainHandler = new Handler(Looper.getMainLooper());
78
Robert Snoeberger71e50792019-02-15 15:48:01 -050079 /**
Robert Snoeberger15b4af12019-01-18 15:37:27 -050080 * Observe settings changes to know when to switch the clock face.
81 */
82 private final ContentObserver mContentObserver =
Robert Snoeberger9ad03f42019-02-28 14:47:49 -050083 new ContentObserver(mMainHandler) {
Robert Snoeberger15b4af12019-01-18 15:37:27 -050084 @Override
85 public void onChange(boolean selfChange) {
86 super.onChange(selfChange);
Robert Snoeberger71e50792019-02-15 15:48:01 -050087 reload();
Robert Snoeberger15b4af12019-01-18 15:37:27 -050088 }
89 };
Robert Snoeberger9ad03f42019-02-28 14:47:49 -050090
91 private final PluginListener<ClockPlugin> mClockPluginListener =
92 new PluginListener<ClockPlugin>() {
93 @Override
94 public void onPluginConnected(ClockPlugin plugin, Context pluginContext) {
95 addClockPlugin(plugin);
96 reload();
97 }
98
99 @Override
100 public void onPluginDisconnected(ClockPlugin plugin) {
101 removeClockPlugin(plugin);
102 reload();
103 }
104 };
105
106 private final PluginManager mPluginManager;
107
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000108 /**
109 * Observe changes to dock state to know when to switch the clock face.
110 */
111 private final DockEventListener mDockEventListener =
112 new DockEventListener() {
113 @Override
114 public void onEvent(int event) {
Robert Snoeberger71e50792019-02-15 15:48:01 -0500115 mIsDocked = (event == DockManager.STATE_DOCKED
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000116 || event == DockManager.STATE_DOCKED_HIDE);
Robert Snoeberger71e50792019-02-15 15:48:01 -0500117 reload();
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000118 }
119 };
Robert Snoeberger71e50792019-02-15 15:48:01 -0500120 @Nullable private final DockManager mDockManager;
121 /**
122 * When docked, the DOCKED_CLOCK_FACE setting will be checked for the custom clock face
123 * to show.
124 */
125 private boolean mIsDocked;
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500126
127 private final List<ClockChangedListener> mListeners = new ArrayList<>();
128
Robert Snoeberger71e50792019-02-15 15:48:01 -0500129 private final SysuiColorExtractor mColorExtractor;
130 private final int mWidth;
131 private final int mHeight;
132
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500133 @Inject
Robert Snoeberger71e50792019-02-15 15:48:01 -0500134 public ClockManager(Context context, InjectionInflationController injectionInflater,
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500135 PluginManager pluginManager, @Nullable DockManager dockManager,
136 SysuiColorExtractor colorExtractor) {
137 this(context, injectionInflater, pluginManager, dockManager, colorExtractor,
138 context.getContentResolver(), new SettingsWrapper(context.getContentResolver()));
Robert Snoeberger71e50792019-02-15 15:48:01 -0500139 }
140
141 ClockManager(Context context, InjectionInflationController injectionInflater,
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500142 PluginManager pluginManager, @Nullable DockManager dockManager,
143 SysuiColorExtractor colorExtractor, ContentResolver contentResolver,
144 SettingsWrapper settingsWrapper) {
145 mPluginManager = pluginManager;
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000146 mDockManager = dockManager;
Robert Snoeberger71e50792019-02-15 15:48:01 -0500147 mColorExtractor = colorExtractor;
148 mContentResolver = contentResolver;
149 mSettingsWrapper = settingsWrapper;
Robert Snoeberger6b244b02019-02-04 15:33:31 -0500150
151 Resources res = context.getResources();
Robert Snoeberger71e50792019-02-15 15:48:01 -0500152 LayoutInflater layoutInflater = injectionInflater.injectable(LayoutInflater.from(context));
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500153
154 addClockPlugin(new DefaultClockController(res, layoutInflater));
155 addClockPlugin(new BubbleClockController(res, layoutInflater));
156 addClockPlugin(new StretchAnalogClockController(res, layoutInflater));
157 addClockPlugin(new TypeClockController(res, layoutInflater));
Robert Snoeberger71e50792019-02-15 15:48:01 -0500158
159 // Store the size of the display for generation of clock preview.
160 DisplayMetrics dm = res.getDisplayMetrics();
161 mWidth = dm.widthPixels;
162 mHeight = dm.heightPixels;
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500163 }
164
165 /**
166 * Add listener to be notified when clock implementation should change.
167 */
168 public void addOnClockChangedListener(ClockChangedListener listener) {
169 if (mListeners.isEmpty()) {
170 register();
171 }
172 mListeners.add(listener);
Robert Snoeberger71e50792019-02-15 15:48:01 -0500173 reload();
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500174 }
175
176 /**
177 * Remove listener added with {@link addOnClockChangedListener}.
178 */
179 public void removeOnClockChangedListener(ClockChangedListener listener) {
180 mListeners.remove(listener);
181 if (mListeners.isEmpty()) {
182 unregister();
183 }
184 }
185
Robert Snoeberger6b244b02019-02-04 15:33:31 -0500186 /**
187 * Get information about available clock faces.
188 */
189 List<ClockInfo> getClockInfos() {
190 return mClockInfos;
191 }
192
Robert Snoeberger71e50792019-02-15 15:48:01 -0500193 /**
194 * Get the current clock.
195 * @returns current custom clock or null for default.
196 */
197 @Nullable
198 ClockPlugin getCurrentClock() {
199 return mCurrentClock;
200 }
201
202 @VisibleForTesting
203 boolean isDocked() {
204 return mIsDocked;
205 }
206
207 @VisibleForTesting
208 ContentObserver getContentObserver() {
209 return mContentObserver;
210 }
211
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500212 private void addClockPlugin(ClockPlugin plugin) {
213 final String id = plugin.getClass().getName();
214 mClocks.put(plugin.getClass().getName(), plugin);
215 mClockInfos.add(ClockInfo.builder()
216 .setName(plugin.getName())
217 .setTitle(plugin.getTitle())
218 .setId(id)
219 .setThumbnail(() -> plugin.getThumbnail())
220 .setPreview(() -> getClockPreview(id))
221 .build());
222 }
223
224 private void removeClockPlugin(ClockPlugin plugin) {
225 final String id = plugin.getClass().getName();
226 mClocks.remove(id);
227 for (int i = 0; i < mClockInfos.size(); i++) {
228 if (id.equals(mClockInfos.get(i).getId())) {
229 mClockInfos.remove(i);
230 break;
231 }
232 }
233 }
234
Robert Snoeberger71e50792019-02-15 15:48:01 -0500235 /**
236 * Generate a realistic preview of a clock face.
237 * @param clockId ID of clock to use for preview, should be obtained from {@link getClockInfos}.
238 * Returns null if clockId is not found.
239 */
240 @Nullable
241 private Bitmap getClockPreview(String clockId) {
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500242 FutureTask<Bitmap> task = new FutureTask<>(new Callable<Bitmap>() {
243 @Override
244 public Bitmap call() {
245 Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888);
246 ClockPlugin plugin = mClocks.get(clockId);
247 if (plugin == null) {
248 return null;
249 }
Robert Snoeberger71e50792019-02-15 15:48:01 -0500250
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500251 // Use the big clock view for the preview
252 View clockView = plugin.getBigClockView();
253 if (clockView == null) {
254 return null;
255 }
256
257 // Initialize state of plugin before generating preview.
258 plugin.setDarkAmount(1f);
259 plugin.setTextColor(Color.WHITE);
260
261 ColorExtractor.GradientColors colors = mColorExtractor.getColors(
262 WallpaperManager.FLAG_LOCK, true);
263 plugin.setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
264 plugin.onTimeTick();
265
266 // Draw clock view hierarchy to canvas.
267 Canvas canvas = new Canvas(bitmap);
268 canvas.drawColor(Color.BLACK);
269 dispatchVisibilityAggregated(clockView, true);
270 clockView.measure(MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY),
271 MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
272 clockView.layout(0, 0, mWidth, mHeight);
273 clockView.draw(canvas);
274 return bitmap;
275 }
276 });
277
278 if (Looper.myLooper() == Looper.getMainLooper()) {
279 task.run();
280 } else {
281 mMainHandler.post(task);
Robert Snoeberger71e50792019-02-15 15:48:01 -0500282 }
283
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500284 try {
285 return task.get();
286 } catch (Exception e) {
287 Log.e(TAG, "Error completing task", e);
288 return null;
289 }
Robert Snoeberger71e50792019-02-15 15:48:01 -0500290 }
291
Robert Snoeberger60cbc962019-02-20 15:46:43 -0500292 private void dispatchVisibilityAggregated(View view, boolean isVisible) {
293 // Similar to View.dispatchVisibilityAggregated implementation.
294 final boolean thisVisible = view.getVisibility() == View.VISIBLE;
295 if (thisVisible || !isVisible) {
296 view.onVisibilityAggregated(isVisible);
297 }
298
299 if (view instanceof ViewGroup) {
300 isVisible = thisVisible && isVisible;
301 ViewGroup vg = (ViewGroup) view;
302 int count = vg.getChildCount();
303
304 for (int i = 0; i < count; i++) {
305 dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
306 }
307 }
308 }
309
Robert Snoeberger71e50792019-02-15 15:48:01 -0500310 private void notifyClockChanged(ClockPlugin plugin) {
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500311 for (int i = 0; i < mListeners.size(); i++) {
312 // It probably doesn't make sense to supply the same plugin instances to multiple
313 // listeners. This should be fine for now since there is only a single listener.
314 mListeners.get(i).onClockChanged(plugin);
315 }
316 }
317
318 private void register() {
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500319 mPluginManager.addPluginListener(mClockPluginListener, ClockPlugin.class, true);
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500320 mContentResolver.registerContentObserver(
321 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
322 false, mContentObserver);
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000323 mContentResolver.registerContentObserver(
324 Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE),
325 false, mContentObserver);
326 if (mDockManager != null) {
327 mDockManager.addListener(mDockEventListener);
328 }
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500329 }
330
331 private void unregister() {
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500332 mPluginManager.removePluginListener(mClockPluginListener);
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500333 mContentResolver.unregisterContentObserver(mContentObserver);
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000334 if (mDockManager != null) {
335 mDockManager.removeListener(mDockEventListener);
336 }
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500337 }
338
Robert Snoeberger71e50792019-02-15 15:48:01 -0500339 private void reload() {
340 mCurrentClock = getClockPlugin();
Robert Snoeberger00257512019-02-27 16:44:04 -0500341 if (mCurrentClock instanceof DefaultClockController) {
342 notifyClockChanged(null);
343 } else {
344 notifyClockChanged(mCurrentClock);
345 }
Robert Snoeberger71e50792019-02-15 15:48:01 -0500346 }
347
348 private ClockPlugin getClockPlugin() {
349 ClockPlugin plugin = null;
350 if (mIsDocked) {
351 final String name = mSettingsWrapper.getDockedClockFace();
352 if (name != null) {
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500353 plugin = mClocks.get(name);
354 if (plugin != null) {
355 return plugin;
Robert Snoeberger71e50792019-02-15 15:48:01 -0500356 }
357 }
358 }
359 final String name = mSettingsWrapper.getLockScreenCustomClockFace();
360 if (name != null) {
Robert Snoeberger9ad03f42019-02-28 14:47:49 -0500361 plugin = mClocks.get(name);
Robert Snoeberger71e50792019-02-15 15:48:01 -0500362 }
363 return plugin;
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000364 }
365
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500366 /**
367 * Listener for events that should cause the custom clock face to change.
368 */
369 public interface ClockChangedListener {
370 /**
371 * Called when custom clock should change.
372 *
373 * @param clock Custom clock face to use. A null value indicates the default clock face.
374 */
375 void onClockChanged(ClockPlugin clock);
376 }
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500377}