• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.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          */
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             reload();
291             if (plugin == mCurrentClock) {
292                 ClockManager.this.reload();
293             }
294         }
295 
296         @Override
onPluginDisconnected(ClockPlugin plugin)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
getCurrentClock()311         ClockPlugin getCurrentClock() {
312             return mCurrentClock;
313         }
314 
315         /**
316          * Get information about available clock faces.
317          */
getInfo()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          */
addClockPlugin(ClockPlugin plugin)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 
removeClockPlugin(ClockPlugin plugin)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          */
reload()353         void reload() {
354             mCurrentClock = getClockPlugin();
355         }
356 
getClockPlugin()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