Home | History | Annotate | Download | only in clock
      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