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 18 import android.annotation.Nullable; 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.database.ContentObserver; 23 import android.net.Uri; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.UserHandle; 27 import android.provider.Settings; 28 import android.util.ArrayMap; 29 import android.util.DisplayMetrics; 30 import android.view.LayoutInflater; 31 32 import androidx.annotation.VisibleForTesting; 33 import androidx.lifecycle.Observer; 34 35 import com.android.systemui.colorextraction.SysuiColorExtractor; 36 import com.android.systemui.dock.DockManager; 37 import com.android.systemui.dock.DockManager.DockEventListener; 38 import com.android.systemui.plugins.ClockPlugin; 39 import com.android.systemui.plugins.PluginListener; 40 import com.android.systemui.settings.CurrentUserObservable; 41 import com.android.systemui.shared.plugins.PluginManager; 42 import com.android.systemui.util.InjectionInflationController; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.function.Supplier; 49 50 import javax.inject.Inject; 51 import javax.inject.Singleton; 52 53 /** 54 * Manages custom clock faces for AOD and lock screen. 55 */ 56 @Singleton 57 public final class ClockManager { 58 59 private static final String TAG = "ClockOptsProvider"; 60 61 private final AvailableClocks mPreviewClocks; 62 private final List<Supplier<ClockPlugin>> mBuiltinClocks = new ArrayList<>(); 63 64 private final Context mContext; 65 private final ContentResolver mContentResolver; 66 private final SettingsWrapper mSettingsWrapper; 67 private final Handler mMainHandler = new Handler(Looper.getMainLooper()); 68 private final CurrentUserObservable mCurrentUserObservable; 69 70 /** 71 * Observe settings changes to know when to switch the clock face. 72 */ 73 private final ContentObserver mContentObserver = 74 new ContentObserver(mMainHandler) { 75 @Override 76 public void onChange(boolean selfChange, Uri uri, int userId) { 77 super.onChange(selfChange, uri, userId); 78 if (Objects.equals(userId, 79 mCurrentUserObservable.getCurrentUser().getValue())) { 80 reload(); 81 } 82 } 83 }; 84 85 /** 86 * Observe user changes and react by potentially loading the custom clock for the new user. 87 */ 88 private final Observer<Integer> mCurrentUserObserver = (newUserId) -> reload(); 89 90 private final PluginManager mPluginManager; 91 @Nullable private final DockManager mDockManager; 92 93 /** 94 * Observe changes to dock state to know when to switch the clock face. 95 */ 96 private final DockEventListener mDockEventListener = 97 new DockEventListener() { 98 @Override 99 public void onEvent(int event) { 100 mIsDocked = (event == DockManager.STATE_DOCKED 101 || event == DockManager.STATE_DOCKED_HIDE); 102 reload(); 103 } 104 }; 105 106 /** 107 * When docked, the DOCKED_CLOCK_FACE setting will be checked for the custom clock face 108 * to show. 109 */ 110 private boolean mIsDocked; 111 112 /** 113 * Listeners for onClockChanged event. 114 * 115 * Each listener must receive a separate clock plugin instance. Otherwise, there could be 116 * problems like attempting to attach a view that already has a parent. To deal with this issue, 117 * each listener is associated with a collection of available clocks. When onClockChanged is 118 * fired the current clock plugin instance is retrieved from that listeners available clocks. 119 */ 120 private final Map<ClockChangedListener, AvailableClocks> mListeners = new ArrayMap<>(); 121 122 private final int mWidth; 123 private final int mHeight; 124 125 @Inject ClockManager(Context context, InjectionInflationController injectionInflater, PluginManager pluginManager, SysuiColorExtractor colorExtractor, @Nullable DockManager dockManager)126 public ClockManager(Context context, InjectionInflationController injectionInflater, 127 PluginManager pluginManager, SysuiColorExtractor colorExtractor, 128 @Nullable DockManager dockManager) { 129 this(context, injectionInflater, pluginManager, colorExtractor, 130 context.getContentResolver(), new CurrentUserObservable(context), 131 new SettingsWrapper(context.getContentResolver()), dockManager); 132 } 133 134 @VisibleForTesting ClockManager(Context context, InjectionInflationController injectionInflater, PluginManager pluginManager, SysuiColorExtractor colorExtractor, ContentResolver contentResolver, CurrentUserObservable currentUserObservable, SettingsWrapper settingsWrapper, DockManager dockManager)135 ClockManager(Context context, InjectionInflationController injectionInflater, 136 PluginManager pluginManager, SysuiColorExtractor colorExtractor, 137 ContentResolver contentResolver, CurrentUserObservable currentUserObservable, 138 SettingsWrapper settingsWrapper, DockManager dockManager) { 139 mContext = context; 140 mPluginManager = pluginManager; 141 mContentResolver = contentResolver; 142 mSettingsWrapper = settingsWrapper; 143 mCurrentUserObservable = currentUserObservable; 144 mDockManager = dockManager; 145 mPreviewClocks = new AvailableClocks(); 146 147 Resources res = context.getResources(); 148 LayoutInflater layoutInflater = injectionInflater.injectable(LayoutInflater.from(context)); 149 150 addBuiltinClock(() -> new DefaultClockController(res, layoutInflater, colorExtractor)); 151 addBuiltinClock(() -> new BubbleClockController(res, layoutInflater, colorExtractor)); 152 addBuiltinClock(() -> new AnalogClockController(res, layoutInflater, colorExtractor)); 153 154 // Store the size of the display for generation of clock preview. 155 DisplayMetrics dm = res.getDisplayMetrics(); 156 mWidth = dm.widthPixels; 157 mHeight = dm.heightPixels; 158 } 159 160 /** 161 * Add listener to be notified when clock implementation should change. 162 */ addOnClockChangedListener(ClockChangedListener listener)163 public void addOnClockChangedListener(ClockChangedListener listener) { 164 if (mListeners.isEmpty()) { 165 register(); 166 } 167 AvailableClocks availableClocks = new AvailableClocks(); 168 for (int i = 0; i < mBuiltinClocks.size(); i++) { 169 availableClocks.addClockPlugin(mBuiltinClocks.get(i).get()); 170 } 171 mListeners.put(listener, availableClocks); 172 mPluginManager.addPluginListener(availableClocks, ClockPlugin.class, true); 173 reload(); 174 } 175 176 /** 177 * Remove listener added with {@link addOnClockChangedListener}. 178 */ removeOnClockChangedListener(ClockChangedListener listener)179 public void removeOnClockChangedListener(ClockChangedListener listener) { 180 AvailableClocks availableClocks = mListeners.remove(listener); 181 mPluginManager.removePluginListener(availableClocks); 182 if (mListeners.isEmpty()) { 183 unregister(); 184 } 185 } 186 187 /** 188 * Get information about available clock faces. 189 */ getClockInfos()190 List<ClockInfo> getClockInfos() { 191 return mPreviewClocks.getInfo(); 192 } 193 194 /** 195 * Get the current clock. 196 * @return current custom clock or null for default. 197 */ 198 @Nullable getCurrentClock()199 ClockPlugin getCurrentClock() { 200 return mPreviewClocks.getCurrentClock(); 201 } 202 203 @VisibleForTesting isDocked()204 boolean isDocked() { 205 return mIsDocked; 206 } 207 208 @VisibleForTesting getContentObserver()209 ContentObserver getContentObserver() { 210 return mContentObserver; 211 } 212 addBuiltinClock(Supplier<ClockPlugin> pluginSupplier)213 private void addBuiltinClock(Supplier<ClockPlugin> pluginSupplier) { 214 ClockPlugin plugin = pluginSupplier.get(); 215 mPreviewClocks.addClockPlugin(plugin); 216 mBuiltinClocks.add(pluginSupplier); 217 } 218 register()219 private void register() { 220 mPluginManager.addPluginListener(mPreviewClocks, ClockPlugin.class, true); 221 mContentResolver.registerContentObserver( 222 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), 223 false, mContentObserver, UserHandle.USER_ALL); 224 mContentResolver.registerContentObserver( 225 Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE), 226 false, mContentObserver, UserHandle.USER_ALL); 227 mCurrentUserObservable.getCurrentUser().observeForever(mCurrentUserObserver); 228 if (mDockManager != null) { 229 mDockManager.addListener(mDockEventListener); 230 } 231 } 232 unregister()233 private void unregister() { 234 mPluginManager.removePluginListener(mPreviewClocks); 235 mContentResolver.unregisterContentObserver(mContentObserver); 236 mCurrentUserObservable.getCurrentUser().removeObserver(mCurrentUserObserver); 237 if (mDockManager != null) { 238 mDockManager.removeListener(mDockEventListener); 239 } 240 } 241 reload()242 private void reload() { 243 mPreviewClocks.reloadCurrentClock(); 244 mListeners.forEach((listener, clocks) -> { 245 clocks.reloadCurrentClock(); 246 final ClockPlugin clock = clocks.getCurrentClock(); 247 if (Looper.myLooper() == Looper.getMainLooper()) { 248 listener.onClockChanged(clock instanceof DefaultClockController ? null : clock); 249 } else { 250 mMainHandler.post(() -> listener.onClockChanged( 251 clock instanceof DefaultClockController ? null : clock)); 252 } 253 }); 254 } 255 256 /** 257 * Listener for events that should cause the custom clock face to change. 258 */ 259 public interface ClockChangedListener { 260 /** 261 * Called when custom clock should change. 262 * 263 * @param clock Custom clock face to use. A null value indicates the default clock face. 264 */ onClockChanged(ClockPlugin clock)265 void onClockChanged(ClockPlugin clock); 266 } 267 268 /** 269 * Collection of available clocks. 270 */ 271 private final class AvailableClocks implements PluginListener<ClockPlugin> { 272 273 /** 274 * Map from expected value stored in settings to plugin for custom clock face. 275 */ 276 private final Map<String, ClockPlugin> mClocks = new ArrayMap<>(); 277 278 /** 279 * Metadata about available clocks, such as name and preview images. 280 */ 281 private final List<ClockInfo> mClockInfo = new ArrayList<>(); 282 283 /** 284 * Active ClockPlugin. 285 */ 286 @Nullable private ClockPlugin mCurrentClock; 287 288 @Override onPluginConnected(ClockPlugin plugin, Context pluginContext)289 public void onPluginConnected(ClockPlugin plugin, Context pluginContext) { 290 addClockPlugin(plugin); 291 reloadIfNeeded(plugin); 292 } 293 294 @Override onPluginDisconnected(ClockPlugin plugin)295 public void onPluginDisconnected(ClockPlugin plugin) { 296 removeClockPlugin(plugin); 297 reloadIfNeeded(plugin); 298 } 299 300 /** 301 * Get the current clock. 302 * @return current custom clock or null for default. 303 */ 304 @Nullable getCurrentClock()305 ClockPlugin getCurrentClock() { 306 return mCurrentClock; 307 } 308 309 /** 310 * Get information about available clock faces. 311 */ getInfo()312 List<ClockInfo> getInfo() { 313 return mClockInfo; 314 } 315 316 /** 317 * Adds a clock plugin to the collection of available clocks. 318 * 319 * @param plugin The plugin to add. 320 */ addClockPlugin(ClockPlugin plugin)321 void addClockPlugin(ClockPlugin plugin) { 322 final String id = plugin.getClass().getName(); 323 mClocks.put(plugin.getClass().getName(), plugin); 324 mClockInfo.add(ClockInfo.builder() 325 .setName(plugin.getName()) 326 .setTitle(plugin::getTitle) 327 .setId(id) 328 .setThumbnail(plugin::getThumbnail) 329 .setPreview(() -> plugin.getPreview(mWidth, mHeight)) 330 .build()); 331 } 332 removeClockPlugin(ClockPlugin plugin)333 private void removeClockPlugin(ClockPlugin plugin) { 334 final String id = plugin.getClass().getName(); 335 mClocks.remove(id); 336 for (int i = 0; i < mClockInfo.size(); i++) { 337 if (id.equals(mClockInfo.get(i).getId())) { 338 mClockInfo.remove(i); 339 break; 340 } 341 } 342 } 343 reloadIfNeeded(ClockPlugin plugin)344 private void reloadIfNeeded(ClockPlugin plugin) { 345 final boolean wasCurrentClock = plugin == mCurrentClock; 346 reloadCurrentClock(); 347 final boolean isCurrentClock = plugin == mCurrentClock; 348 if (wasCurrentClock || isCurrentClock) { 349 ClockManager.this.reload(); 350 } 351 } 352 353 /** 354 * Update the current clock. 355 */ reloadCurrentClock()356 void reloadCurrentClock() { 357 mCurrentClock = getClockPlugin(); 358 } 359 getClockPlugin()360 private ClockPlugin getClockPlugin() { 361 ClockPlugin plugin = null; 362 if (ClockManager.this.isDocked()) { 363 final String name = mSettingsWrapper.getDockedClockFace( 364 mCurrentUserObservable.getCurrentUser().getValue()); 365 if (name != null) { 366 plugin = mClocks.get(name); 367 if (plugin != null) { 368 return plugin; 369 } 370 } 371 } 372 final String name = mSettingsWrapper.getLockScreenCustomClockFace( 373 mCurrentUserObservable.getCurrentUser().getValue()); 374 if (name != null) { 375 plugin = mClocks.get(name); 376 } 377 return plugin; 378 } 379 } 380 } 381