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