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