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 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 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 */ 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 */ 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 */ 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 199 ClockPlugin getCurrentClock() { 200 return mPreviewClocks.getCurrentClock(); 201 } 202 203 @VisibleForTesting 204 boolean isDocked() { 205 return mIsDocked; 206 } 207 208 @VisibleForTesting 209 ContentObserver getContentObserver() { 210 return mContentObserver; 211 } 212 213 private void addBuiltinClock(Supplier<ClockPlugin> pluginSupplier) { 214 ClockPlugin plugin = pluginSupplier.get(); 215 mPreviewClocks.addClockPlugin(plugin); 216 mBuiltinClocks.add(pluginSupplier); 217 } 218 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 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 242 private void reload() { 243 mPreviewClocks.reload(); 244 mListeners.forEach((listener, clocks) -> { 245 clocks.reload(); 246 ClockPlugin clock = clocks.getCurrentClock(); 247 if (clock instanceof DefaultClockController) { 248 listener.onClockChanged(null); 249 } else { 250 listener.onClockChanged(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 */ 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 288 public void onPluginConnected(ClockPlugin plugin, Context pluginContext) { 289 addClockPlugin(plugin); 290 reload(); 291 if (plugin == mCurrentClock) { 292 ClockManager.this.reload(); 293 } 294 } 295 296 @Override 297 public void onPluginDisconnected(ClockPlugin plugin) { 298 boolean isCurrentClock = plugin == mCurrentClock; 299 removeClockPlugin(plugin); 300 reload(); 301 if (isCurrentClock) { 302 ClockManager.this.reload(); 303 } 304 } 305 306 /** 307 * Get the current clock. 308 * @return current custom clock or null for default. 309 */ 310 @Nullable 311 ClockPlugin getCurrentClock() { 312 return mCurrentClock; 313 } 314 315 /** 316 * Get information about available clock faces. 317 */ 318 List<ClockInfo> getInfo() { 319 return mClockInfo; 320 } 321 322 /** 323 * Adds a clock plugin to the collection of available clocks. 324 * 325 * @param plugin The plugin to add. 326 */ 327 void addClockPlugin(ClockPlugin plugin) { 328 final String id = plugin.getClass().getName(); 329 mClocks.put(plugin.getClass().getName(), plugin); 330 mClockInfo.add(ClockInfo.builder() 331 .setName(plugin.getName()) 332 .setTitle(plugin.getTitle()) 333 .setId(id) 334 .setThumbnail(plugin::getThumbnail) 335 .setPreview(() -> plugin.getPreview(mWidth, mHeight)) 336 .build()); 337 } 338 339 private void removeClockPlugin(ClockPlugin plugin) { 340 final String id = plugin.getClass().getName(); 341 mClocks.remove(id); 342 for (int i = 0; i < mClockInfo.size(); i++) { 343 if (id.equals(mClockInfo.get(i).getId())) { 344 mClockInfo.remove(i); 345 break; 346 } 347 } 348 } 349 350 /** 351 * Update the current clock. 352 */ 353 void reload() { 354 mCurrentClock = getClockPlugin(); 355 } 356 357 private ClockPlugin getClockPlugin() { 358 ClockPlugin plugin = null; 359 if (ClockManager.this.isDocked()) { 360 final String name = mSettingsWrapper.getDockedClockFace( 361 mCurrentUserObservable.getCurrentUser().getValue()); 362 if (name != null) { 363 plugin = mClocks.get(name); 364 if (plugin != null) { 365 return plugin; 366 } 367 } 368 } 369 final String name = mSettingsWrapper.getLockScreenCustomClockFace( 370 mCurrentUserObservable.getCurrentUser().getValue()); 371 if (name != null) { 372 plugin = mClocks.get(name); 373 } 374 return plugin; 375 } 376 } 377 } 378