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