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