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