Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 1 | /* |
| 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 | */ |
| 16 | package com.android.keyguard.clock; |
| 17 | |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 18 | import android.annotation.Nullable; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 19 | import android.app.WallpaperManager; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 20 | import android.content.ContentResolver; |
| 21 | import android.content.Context; |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 22 | import android.content.res.Resources; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 23 | import android.database.ContentObserver; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 24 | import android.graphics.Bitmap; |
| 25 | import android.graphics.Bitmap.Config; |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 26 | import android.graphics.BitmapFactory; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 27 | import android.graphics.Canvas; |
| 28 | import android.graphics.Color; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 29 | import android.os.Handler; |
| 30 | import android.os.Looper; |
| 31 | import android.provider.Settings; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 32 | import android.util.ArrayMap; |
| 33 | import android.util.DisplayMetrics; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 34 | import android.view.LayoutInflater; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 35 | import android.view.View; |
| 36 | import android.view.View.MeasureSpec; |
Robert Snoeberger | 60cbc96 | 2019-02-20 15:46:43 -0500 | [diff] [blame] | 37 | import android.view.ViewGroup; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 38 | |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 39 | import androidx.annotation.VisibleForTesting; |
| 40 | |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 41 | import com.android.internal.colorextraction.ColorExtractor; |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 42 | import com.android.keyguard.R; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 43 | import com.android.systemui.colorextraction.SysuiColorExtractor; |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 44 | import com.android.systemui.dock.DockManager; |
| 45 | import com.android.systemui.dock.DockManager.DockEventListener; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 46 | import com.android.systemui.plugins.ClockPlugin; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 47 | import com.android.systemui.util.InjectionInflationController; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 48 | |
| 49 | import java.util.ArrayList; |
| 50 | import java.util.List; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 51 | import java.util.Map; |
| 52 | import java.util.function.Supplier; |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 53 | |
| 54 | import javax.inject.Inject; |
| 55 | import javax.inject.Singleton; |
| 56 | |
| 57 | /** |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 58 | * Manages custom clock faces for AOD and lock screen. |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 59 | */ |
| 60 | @Singleton |
| 61 | public final class ClockManager { |
| 62 | |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 63 | private final List<ClockInfo> mClockInfos = new ArrayList<>(); |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 64 | /** |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 65 | * 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 Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 73 | * 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 Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 80 | reload(); |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 81 | } |
| 82 | }; |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 83 | /** |
| 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 Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 90 | mIsDocked = (event == DockManager.STATE_DOCKED |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 91 | || event == DockManager.STATE_DOCKED_HIDE); |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 92 | reload(); |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 93 | } |
| 94 | }; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 95 | @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 Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 101 | |
| 102 | private final List<ClockChangedListener> mListeners = new ArrayList<>(); |
| 103 | |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 104 | private final SysuiColorExtractor mColorExtractor; |
| 105 | private final int mWidth; |
| 106 | private final int mHeight; |
| 107 | |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 108 | @Inject |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 109 | 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 Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 118 | mDockManager = dockManager; |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 119 | mColorExtractor = colorExtractor; |
| 120 | mContentResolver = contentResolver; |
| 121 | mSettingsWrapper = settingsWrapper; |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 122 | |
| 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 Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 136 | .setPreview(() -> getClockPreview(BubbleClockController.class.getName())) |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 137 | .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 Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 143 | .setPreview(() -> getClockPreview(StretchAnalogClockController.class.getName())) |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 144 | .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 Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 150 | .setPreview(() -> getClockPreview(TypeClockController.class.getName())) |
Robert Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 151 | .build()); |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 152 | |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 153 | 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 Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 165 | } |
| 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 Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 175 | reload(); |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 176 | } |
| 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 Snoeberger | 6b244b0 | 2019-02-04 15:33:31 -0500 | [diff] [blame] | 188 | /** |
| 189 | * Get information about available clock faces. |
| 190 | */ |
| 191 | List<ClockInfo> getClockInfos() { |
| 192 | return mClockInfos; |
| 193 | } |
| 194 | |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 195 | /** |
| 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 Snoeberger | 60cbc96 | 2019-02-20 15:46:43 -0500 | [diff] [blame] | 245 | dispatchVisibilityAggregated(clockView, true); |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 246 | 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 Snoeberger | 60cbc96 | 2019-02-20 15:46:43 -0500 | [diff] [blame] | 255 | 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 Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 273 | private void notifyClockChanged(ClockPlugin plugin) { |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 274 | 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 Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 285 | mContentResolver.registerContentObserver( |
| 286 | Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE), |
| 287 | false, mContentObserver); |
| 288 | if (mDockManager != null) { |
| 289 | mDockManager.addListener(mDockEventListener); |
| 290 | } |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 291 | } |
| 292 | |
| 293 | private void unregister() { |
| 294 | mContentResolver.unregisterContentObserver(mContentObserver); |
Robert Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 295 | if (mDockManager != null) { |
| 296 | mDockManager.removeListener(mDockEventListener); |
| 297 | } |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 298 | } |
| 299 | |
Robert Snoeberger | 71e5079 | 2019-02-15 15:48:01 -0500 | [diff] [blame] | 300 | 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 Snoeberger | b300a4e | 2019-02-13 20:13:53 +0000 | [diff] [blame] | 327 | } |
| 328 | |
Robert Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 329 | /** |
| 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 Snoeberger | 15b4af1 | 2019-01-18 15:37:27 -0500 | [diff] [blame] | 340 | } |