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