blob: 3827e445c136401453fce2c44965f8e4411b43df [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 Snoeberger6b244b02019-02-04 15:33:31 -050026import android.graphics.BitmapFactory;
Robert Snoeberger71e50792019-02-15 15:48:01 -050027import android.graphics.Canvas;
28import android.graphics.Color;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050029import android.os.Handler;
30import android.os.Looper;
31import android.provider.Settings;
Robert Snoeberger71e50792019-02-15 15:48:01 -050032import android.util.ArrayMap;
33import android.util.DisplayMetrics;
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 Snoeberger6b244b02019-02-04 15:33:31 -050042import com.android.keyguard.R;
Robert Snoeberger71e50792019-02-15 15:48:01 -050043import com.android.systemui.colorextraction.SysuiColorExtractor;
Robert Snoebergerb300a4e2019-02-13 20:13:53 +000044import com.android.systemui.dock.DockManager;
45import com.android.systemui.dock.DockManager.DockEventListener;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050046import com.android.systemui.plugins.ClockPlugin;
Robert Snoeberger71e50792019-02-15 15:48:01 -050047import com.android.systemui.util.InjectionInflationController;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050048
49import java.util.ArrayList;
50import java.util.List;
Robert Snoeberger71e50792019-02-15 15:48:01 -050051import java.util.Map;
52import java.util.function.Supplier;
Robert Snoeberger15b4af12019-01-18 15:37:27 -050053
54import javax.inject.Inject;
55import javax.inject.Singleton;
56
57/**
Robert Snoeberger71e50792019-02-15 15:48:01 -050058 * Manages custom clock faces for AOD and lock screen.
Robert Snoeberger15b4af12019-01-18 15:37:27 -050059 */
60@Singleton
61public final class ClockManager {
62
Robert Snoeberger6b244b02019-02-04 15:33:31 -050063 private final List<ClockInfo> mClockInfos = new ArrayList<>();
Robert Snoeberger15b4af12019-01-18 15:37:27 -050064 /**
Robert Snoeberger71e50792019-02-15 15:48:01 -050065 * Map from expected value stored in settings to supplier of custom clock face.
66 */
67 private final Map<String, Supplier<ClockPlugin>> mClocks = new ArrayMap<>();
68 @Nullable private ClockPlugin mCurrentClock;
69
70 private final ContentResolver mContentResolver;
71 private final SettingsWrapper mSettingsWrapper;
72 /**
Robert Snoeberger15b4af12019-01-18 15:37:27 -050073 * Observe settings changes to know when to switch the clock face.
74 */
75 private final ContentObserver mContentObserver =
76 new ContentObserver(new Handler(Looper.getMainLooper())) {
77 @Override
78 public void onChange(boolean selfChange) {
79 super.onChange(selfChange);
Robert Snoeberger71e50792019-02-15 15:48:01 -050080 reload();
Robert Snoeberger15b4af12019-01-18 15:37:27 -050081 }
82 };
Robert Snoebergerb300a4e2019-02-13 20:13:53 +000083 /**
84 * Observe changes to dock state to know when to switch the clock face.
85 */
86 private final DockEventListener mDockEventListener =
87 new DockEventListener() {
88 @Override
89 public void onEvent(int event) {
Robert Snoeberger71e50792019-02-15 15:48:01 -050090 mIsDocked = (event == DockManager.STATE_DOCKED
Robert Snoebergerb300a4e2019-02-13 20:13:53 +000091 || event == DockManager.STATE_DOCKED_HIDE);
Robert Snoeberger71e50792019-02-15 15:48:01 -050092 reload();
Robert Snoebergerb300a4e2019-02-13 20:13:53 +000093 }
94 };
Robert Snoeberger71e50792019-02-15 15:48:01 -050095 @Nullable private final DockManager mDockManager;
96 /**
97 * When docked, the DOCKED_CLOCK_FACE setting will be checked for the custom clock face
98 * to show.
99 */
100 private boolean mIsDocked;
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500101
102 private final List<ClockChangedListener> mListeners = new ArrayList<>();
103
Robert Snoeberger71e50792019-02-15 15:48:01 -0500104 private final SysuiColorExtractor mColorExtractor;
105 private final int mWidth;
106 private final int mHeight;
107
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500108 @Inject
Robert Snoeberger71e50792019-02-15 15:48:01 -0500109 public ClockManager(Context context, InjectionInflationController injectionInflater,
110 @Nullable DockManager dockManager, SysuiColorExtractor colorExtractor) {
111 this(context, injectionInflater, dockManager, colorExtractor, context.getContentResolver(),
112 new SettingsWrapper(context.getContentResolver()));
113 }
114
115 ClockManager(Context context, InjectionInflationController injectionInflater,
116 @Nullable DockManager dockManager, SysuiColorExtractor colorExtractor,
117 ContentResolver contentResolver, SettingsWrapper settingsWrapper) {
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000118 mDockManager = dockManager;
Robert Snoeberger71e50792019-02-15 15:48:01 -0500119 mColorExtractor = colorExtractor;
120 mContentResolver = contentResolver;
121 mSettingsWrapper = settingsWrapper;
Robert Snoeberger6b244b02019-02-04 15:33:31 -0500122
123 Resources res = context.getResources();
124 mClockInfos.add(ClockInfo.builder()
125 .setName("default")
126 .setTitle(res.getString(R.string.clock_title_default))
127 .setId("default")
128 .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.default_thumbnail))
129 .setPreview(() -> BitmapFactory.decodeResource(res, R.drawable.default_preview))
130 .build());
131 mClockInfos.add(ClockInfo.builder()
132 .setName("bubble")
133 .setTitle(res.getString(R.string.clock_title_bubble))
134 .setId(BubbleClockController.class.getName())
135 .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.bubble_thumbnail))
Robert Snoeberger71e50792019-02-15 15:48:01 -0500136 .setPreview(() -> getClockPreview(BubbleClockController.class.getName()))
Robert Snoeberger6b244b02019-02-04 15:33:31 -0500137 .build());
138 mClockInfos.add(ClockInfo.builder()
139 .setName("stretch")
140 .setTitle(res.getString(R.string.clock_title_stretch))
141 .setId(StretchAnalogClockController.class.getName())
142 .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.stretch_thumbnail))
Robert Snoeberger71e50792019-02-15 15:48:01 -0500143 .setPreview(() -> getClockPreview(StretchAnalogClockController.class.getName()))
Robert Snoeberger6b244b02019-02-04 15:33:31 -0500144 .build());
145 mClockInfos.add(ClockInfo.builder()
146 .setName("type")
147 .setTitle(res.getString(R.string.clock_title_type))
148 .setId(TypeClockController.class.getName())
149 .setThumbnail(() -> BitmapFactory.decodeResource(res, R.drawable.type_thumbnail))
Robert Snoeberger71e50792019-02-15 15:48:01 -0500150 .setPreview(() -> getClockPreview(TypeClockController.class.getName()))
Robert Snoeberger6b244b02019-02-04 15:33:31 -0500151 .build());
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000152
Robert Snoeberger71e50792019-02-15 15:48:01 -0500153 LayoutInflater layoutInflater = injectionInflater.injectable(LayoutInflater.from(context));
154 mClocks.put(BubbleClockController.class.getName(),
155 () -> BubbleClockController.build(layoutInflater));
156 mClocks.put(StretchAnalogClockController.class.getName(),
157 () -> StretchAnalogClockController.build(layoutInflater));
158 mClocks.put(TypeClockController.class.getName(),
159 () -> TypeClockController.build(layoutInflater));
160
161 // Store the size of the display for generation of clock preview.
162 DisplayMetrics dm = res.getDisplayMetrics();
163 mWidth = dm.widthPixels;
164 mHeight = dm.heightPixels;
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500165 }
166
167 /**
168 * Add listener to be notified when clock implementation should change.
169 */
170 public void addOnClockChangedListener(ClockChangedListener listener) {
171 if (mListeners.isEmpty()) {
172 register();
173 }
174 mListeners.add(listener);
Robert Snoeberger71e50792019-02-15 15:48:01 -0500175 reload();
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500176 }
177
178 /**
179 * Remove listener added with {@link addOnClockChangedListener}.
180 */
181 public void removeOnClockChangedListener(ClockChangedListener listener) {
182 mListeners.remove(listener);
183 if (mListeners.isEmpty()) {
184 unregister();
185 }
186 }
187
Robert Snoeberger6b244b02019-02-04 15:33:31 -0500188 /**
189 * Get information about available clock faces.
190 */
191 List<ClockInfo> getClockInfos() {
192 return mClockInfos;
193 }
194
Robert Snoeberger71e50792019-02-15 15:48:01 -0500195 /**
196 * Get the current clock.
197 * @returns current custom clock or null for default.
198 */
199 @Nullable
200 ClockPlugin getCurrentClock() {
201 return mCurrentClock;
202 }
203
204 @VisibleForTesting
205 boolean isDocked() {
206 return mIsDocked;
207 }
208
209 @VisibleForTesting
210 ContentObserver getContentObserver() {
211 return mContentObserver;
212 }
213
214 /**
215 * Generate a realistic preview of a clock face.
216 * @param clockId ID of clock to use for preview, should be obtained from {@link getClockInfos}.
217 * Returns null if clockId is not found.
218 */
219 @Nullable
220 private Bitmap getClockPreview(String clockId) {
221 Supplier<ClockPlugin> supplier = mClocks.get(clockId);
222 if (supplier == null) {
223 return null;
224 }
225 ClockPlugin plugin = supplier.get();
226
227 // Use the big clock view for the preview
228 View clockView = plugin.getBigClockView();
229 if (clockView == null) {
230 return null;
231 }
232
233 // Initialize state of plugin before generating preview.
234 plugin.setDarkAmount(1f);
235 plugin.setTextColor(Color.WHITE);
236
237 ColorExtractor.GradientColors colors = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK,
238 true);
239 plugin.setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
240 plugin.dozeTimeTick();
241
242 // Draw clock view hierarchy to canvas.
243 Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888);
244 Canvas canvas = new Canvas(bitmap);
Robert Snoeberger60cbc962019-02-20 15:46:43 -0500245 dispatchVisibilityAggregated(clockView, true);
Robert Snoeberger71e50792019-02-15 15:48:01 -0500246 clockView.measure(MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY),
247 MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
248 clockView.layout(0, 0, mWidth, mHeight);
249 canvas.drawColor(Color.BLACK);
250 clockView.draw(canvas);
251
252 return bitmap;
253 }
254
Robert Snoeberger60cbc962019-02-20 15:46:43 -0500255 private void dispatchVisibilityAggregated(View view, boolean isVisible) {
256 // Similar to View.dispatchVisibilityAggregated implementation.
257 final boolean thisVisible = view.getVisibility() == View.VISIBLE;
258 if (thisVisible || !isVisible) {
259 view.onVisibilityAggregated(isVisible);
260 }
261
262 if (view instanceof ViewGroup) {
263 isVisible = thisVisible && isVisible;
264 ViewGroup vg = (ViewGroup) view;
265 int count = vg.getChildCount();
266
267 for (int i = 0; i < count; i++) {
268 dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
269 }
270 }
271 }
272
Robert Snoeberger71e50792019-02-15 15:48:01 -0500273 private void notifyClockChanged(ClockPlugin plugin) {
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500274 for (int i = 0; i < mListeners.size(); i++) {
275 // It probably doesn't make sense to supply the same plugin instances to multiple
276 // listeners. This should be fine for now since there is only a single listener.
277 mListeners.get(i).onClockChanged(plugin);
278 }
279 }
280
281 private void register() {
282 mContentResolver.registerContentObserver(
283 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
284 false, mContentObserver);
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000285 mContentResolver.registerContentObserver(
286 Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE),
287 false, mContentObserver);
288 if (mDockManager != null) {
289 mDockManager.addListener(mDockEventListener);
290 }
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500291 }
292
293 private void unregister() {
294 mContentResolver.unregisterContentObserver(mContentObserver);
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000295 if (mDockManager != null) {
296 mDockManager.removeListener(mDockEventListener);
297 }
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500298 }
299
Robert Snoeberger71e50792019-02-15 15:48:01 -0500300 private void reload() {
301 mCurrentClock = getClockPlugin();
302 notifyClockChanged(mCurrentClock);
303 }
304
305 private ClockPlugin getClockPlugin() {
306 ClockPlugin plugin = null;
307 if (mIsDocked) {
308 final String name = mSettingsWrapper.getDockedClockFace();
309 if (name != null) {
310 Supplier<ClockPlugin> supplier = mClocks.get(name);
311 if (supplier != null) {
312 plugin = supplier.get();
313 if (plugin != null) {
314 return plugin;
315 }
316 }
317 }
318 }
319 final String name = mSettingsWrapper.getLockScreenCustomClockFace();
320 if (name != null) {
321 Supplier<ClockPlugin> supplier = mClocks.get(name);
322 if (supplier != null) {
323 plugin = supplier.get();
324 }
325 }
326 return plugin;
Robert Snoebergerb300a4e2019-02-13 20:13:53 +0000327 }
328
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500329 /**
330 * Listener for events that should cause the custom clock face to change.
331 */
332 public interface ClockChangedListener {
333 /**
334 * Called when custom clock should change.
335 *
336 * @param clock Custom clock face to use. A null value indicates the default clock face.
337 */
338 void onClockChanged(ClockPlugin clock);
339 }
Robert Snoeberger15b4af12019-01-18 15:37:27 -0500340}