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