Home | History | Annotate | Download | only in dashboard
      1 /*
      2  * Copyright (C) 2015 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.settings.dashboard;
     17 
     18 import android.app.Activity;
     19 import android.content.BroadcastReceiver;
     20 import android.content.ComponentName;
     21 import android.content.IntentFilter;
     22 import android.os.Bundle;
     23 import android.os.Handler;
     24 import android.os.HandlerThread;
     25 import android.os.Looper;
     26 import android.os.Message;
     27 import android.os.Process;
     28 import android.support.annotation.VisibleForTesting;
     29 import android.text.TextUtils;
     30 import android.util.ArrayMap;
     31 import android.util.ArraySet;
     32 import android.util.Log;
     33 
     34 import com.android.settings.SettingsActivity;
     35 import com.android.settings.overlay.FeatureFactory;
     36 import com.android.settingslib.drawer.DashboardCategory;
     37 import com.android.settingslib.drawer.Tile;
     38 import com.android.settingslib.utils.ThreadUtils;
     39 
     40 import java.lang.reflect.Field;
     41 import java.util.List;
     42 
     43 public class SummaryLoader {
     44     private static final boolean DEBUG = DashboardSummary.DEBUG;
     45     private static final String TAG = "SummaryLoader";
     46 
     47     public static final String SUMMARY_PROVIDER_FACTORY = "SUMMARY_PROVIDER_FACTORY";
     48 
     49     private final Activity mActivity;
     50     private final ArrayMap<SummaryProvider, ComponentName> mSummaryProviderMap = new ArrayMap<>();
     51     private final ArrayMap<String, CharSequence> mSummaryTextMap = new ArrayMap<>();
     52     private final DashboardFeatureProvider mDashboardFeatureProvider;
     53     private final String mCategoryKey;
     54 
     55     private final Worker mWorker;
     56     private final HandlerThread mWorkerThread;
     57 
     58     private SummaryConsumer mSummaryConsumer;
     59     private boolean mListening;
     60     private boolean mWorkerListening;
     61     private ArraySet<BroadcastReceiver> mReceivers = new ArraySet<>();
     62 
     63     public SummaryLoader(Activity activity, String categoryKey) {
     64         mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
     65                 .getDashboardFeatureProvider(activity);
     66         mCategoryKey = categoryKey;
     67         mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
     68         mWorkerThread.start();
     69         mWorker = new Worker(mWorkerThread.getLooper());
     70         mActivity = activity;
     71     }
     72 
     73     public void release() {
     74         mWorkerThread.quitSafely();
     75         // Make sure we aren't listening.
     76         setListeningW(false);
     77     }
     78 
     79     public void setSummaryConsumer(SummaryConsumer summaryConsumer) {
     80         mSummaryConsumer = summaryConsumer;
     81     }
     82 
     83     public void setSummary(SummaryProvider provider, final CharSequence summary) {
     84         final ComponentName component = mSummaryProviderMap.get(provider);
     85         ThreadUtils.postOnMainThread(() -> {
     86 
     87             final Tile tile = getTileFromCategory(
     88                     mDashboardFeatureProvider.getTilesForCategory(mCategoryKey), component);
     89 
     90             if (tile == null) {
     91                 if (DEBUG) {
     92                     Log.d(TAG, "Can't find tile for " + component);
     93                 }
     94                 return;
     95             }
     96             if (DEBUG) {
     97                 Log.d(TAG, "setSummary " + tile.title + " - " + summary);
     98             }
     99 
    100             updateSummaryIfNeeded(tile, summary);
    101         });
    102     }
    103 
    104     @VisibleForTesting
    105     void updateSummaryIfNeeded(Tile tile, CharSequence summary) {
    106         if (TextUtils.equals(tile.summary, summary)) {
    107             if (DEBUG) {
    108                 Log.d(TAG, "Summary doesn't change, skipping summary update for " + tile.title);
    109             }
    110             return;
    111         }
    112         mSummaryTextMap.put(mDashboardFeatureProvider.getDashboardKeyForTile(tile), summary);
    113         tile.summary = summary;
    114         if (mSummaryConsumer != null) {
    115             mSummaryConsumer.notifySummaryChanged(tile);
    116         } else {
    117             if (DEBUG) {
    118                 Log.d(TAG, "SummaryConsumer is null, skipping summary update for "
    119                         + tile.title);
    120             }
    121         }
    122     }
    123 
    124     /**
    125      * Only call from the main thread.
    126      */
    127     public void setListening(boolean listening) {
    128         if (mListening == listening) {
    129             return;
    130         }
    131         mListening = listening;
    132         // Unregister listeners immediately.
    133         for (int i = 0; i < mReceivers.size(); i++) {
    134             mActivity.unregisterReceiver(mReceivers.valueAt(i));
    135         }
    136         mReceivers.clear();
    137 
    138         mWorker.removeMessages(Worker.MSG_SET_LISTENING);
    139         if (!listening) {
    140             // Stop listen
    141             mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 0 /* listening */).sendToTarget();
    142         } else {
    143             // Start listen
    144             if (mSummaryProviderMap.isEmpty()) {
    145                 // Category not initialized yet, init before starting to listen
    146                 if (!mWorker.hasMessages(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING)) {
    147                     mWorker.sendEmptyMessage(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING);
    148                 }
    149             } else {
    150                 // Category already initialized, start listening immediately
    151                 mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 1 /* listening */).sendToTarget();
    152             }
    153         }
    154     }
    155 
    156     private SummaryProvider getSummaryProvider(Tile tile) {
    157         if (!mActivity.getPackageName().equals(tile.intent.getComponent().getPackageName())) {
    158             // Not within Settings, can't load Summary directly.
    159             // TODO: Load summary indirectly.
    160             return null;
    161         }
    162         Bundle metaData = getMetaData(tile);
    163         if (metaData == null) {
    164             if (DEBUG) Log.d(TAG, "No metadata specified for " + tile.intent.getComponent());
    165             return null;
    166         }
    167         String clsName = metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
    168         if (clsName == null) {
    169             if (DEBUG) Log.d(TAG, "No fragment specified for " + tile.intent.getComponent());
    170             return null;
    171         }
    172         try {
    173             Class<?> cls = Class.forName(clsName);
    174             Field field = cls.getField(SUMMARY_PROVIDER_FACTORY);
    175             SummaryProviderFactory factory = (SummaryProviderFactory) field.get(null);
    176             return factory.createSummaryProvider(mActivity, this);
    177         } catch (ClassNotFoundException e) {
    178             if (DEBUG) Log.d(TAG, "Couldn't find " + clsName, e);
    179         } catch (NoSuchFieldException e) {
    180             if (DEBUG) Log.d(TAG, "Couldn't find " + SUMMARY_PROVIDER_FACTORY, e);
    181         } catch (ClassCastException e) {
    182             if (DEBUG) Log.d(TAG, "Couldn't cast " + SUMMARY_PROVIDER_FACTORY, e);
    183         } catch (IllegalAccessException e) {
    184             if (DEBUG) Log.d(TAG, "Couldn't get " + SUMMARY_PROVIDER_FACTORY, e);
    185         }
    186         return null;
    187     }
    188 
    189     private Bundle getMetaData(Tile tile) {
    190         return tile.metaData;
    191     }
    192 
    193     /**
    194      * Registers a receiver and automatically unregisters it when the activity is stopping.
    195      * This ensures that the receivers are unregistered immediately, since most summary loader
    196      * operations are asynchronous.
    197      */
    198     public void registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter) {
    199         mActivity.runOnUiThread(new Runnable() {
    200             @Override
    201             public void run() {
    202                 if (!mListening) {
    203                     return;
    204                 }
    205                 mReceivers.add(receiver);
    206                 mActivity.registerReceiver(receiver, filter);
    207             }
    208         });
    209     }
    210 
    211     /**
    212      * Updates all tile's summary to latest cached version. This is necessary to handle the case
    213      * where category is updated after summary change.
    214      */
    215     public void updateSummaryToCache(DashboardCategory category) {
    216         if (category == null) {
    217             return;
    218         }
    219         for (Tile tile : category.getTiles()) {
    220             final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
    221             if (mSummaryTextMap.containsKey(key)) {
    222                 tile.summary = mSummaryTextMap.get(key);
    223             }
    224         }
    225     }
    226 
    227     private synchronized void setListeningW(boolean listening) {
    228         if (mWorkerListening == listening) {
    229             return;
    230         }
    231         mWorkerListening = listening;
    232         if (DEBUG) {
    233             Log.d(TAG, "Listening " + listening);
    234         }
    235         for (SummaryProvider p : mSummaryProviderMap.keySet()) {
    236             try {
    237                 p.setListening(listening);
    238             } catch (Exception e) {
    239                 Log.d(TAG, "Problem in setListening", e);
    240             }
    241         }
    242     }
    243 
    244     private synchronized void makeProviderW(Tile tile) {
    245         SummaryProvider provider = getSummaryProvider(tile);
    246         if (provider != null) {
    247             if (DEBUG) Log.d(TAG, "Creating " + tile);
    248             mSummaryProviderMap.put(provider, tile.intent.getComponent());
    249         }
    250     }
    251 
    252     private Tile getTileFromCategory(DashboardCategory category, ComponentName component) {
    253         if (category == null || category.getTilesCount() == 0) {
    254             return null;
    255         }
    256         final List<Tile> tiles = category.getTiles();
    257         final int tileCount = tiles.size();
    258         for (int j = 0; j < tileCount; j++) {
    259             final Tile tile = tiles.get(j);
    260             if (component.equals(tile.intent.getComponent())) {
    261                 return tile;
    262             }
    263         }
    264         return null;
    265     }
    266 
    267 
    268     public interface SummaryProvider {
    269         void setListening(boolean listening);
    270     }
    271 
    272     public interface SummaryConsumer {
    273         void notifySummaryChanged(Tile tile);
    274     }
    275 
    276     public interface SummaryProviderFactory {
    277         SummaryProvider createSummaryProvider(Activity activity, SummaryLoader summaryLoader);
    278     }
    279 
    280     private class Worker extends Handler {
    281         private static final int MSG_GET_CATEGORY_TILES_AND_SET_LISTENING = 1;
    282         private static final int MSG_GET_PROVIDER = 2;
    283         private static final int MSG_SET_LISTENING = 3;
    284 
    285         public Worker(Looper looper) {
    286             super(looper);
    287         }
    288 
    289         @Override
    290         public void handleMessage(Message msg) {
    291             switch (msg.what) {
    292                 case MSG_GET_CATEGORY_TILES_AND_SET_LISTENING:
    293                     final DashboardCategory category =
    294                             mDashboardFeatureProvider.getTilesForCategory(mCategoryKey);
    295                     if (category == null || category.getTilesCount() == 0) {
    296                         return;
    297                     }
    298                     final List<Tile> tiles = category.getTiles();
    299                     for (Tile tile : tiles) {
    300                         makeProviderW(tile);
    301                     }
    302                     setListeningW(true);
    303                     break;
    304                 case MSG_GET_PROVIDER:
    305                     Tile tile = (Tile) msg.obj;
    306                     makeProviderW(tile);
    307                     break;
    308                 case MSG_SET_LISTENING:
    309                     boolean listening = msg.obj != null && msg.obj.equals(1);
    310                     setListeningW(listening);
    311                     break;
    312             }
    313         }
    314     }
    315 }
    316