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