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