1 /* 2 * Copyright (C) 2016 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.Context; 20 import android.content.res.TypedArray; 21 import android.os.Bundle; 22 import android.support.annotation.VisibleForTesting; 23 import android.support.v7.preference.Preference; 24 import android.support.v7.preference.PreferenceManager; 25 import android.support.v7.preference.PreferenceScreen; 26 import android.text.TextUtils; 27 import android.util.ArrayMap; 28 import android.util.ArraySet; 29 import android.util.Log; 30 31 import com.android.settings.SettingsPreferenceFragment; 32 import com.android.settings.core.BasePreferenceController; 33 import com.android.settings.core.PreferenceControllerListHelper; 34 import com.android.settings.overlay.FeatureFactory; 35 import com.android.settings.search.Indexable; 36 import com.android.settingslib.core.AbstractPreferenceController; 37 import com.android.settingslib.core.lifecycle.Lifecycle; 38 import com.android.settingslib.core.lifecycle.LifecycleObserver; 39 import com.android.settingslib.drawer.DashboardCategory; 40 import com.android.settingslib.drawer.SettingsDrawerActivity; 41 import com.android.settingslib.drawer.Tile; 42 import com.android.settingslib.drawer.TileUtils; 43 44 import java.util.ArrayList; 45 import java.util.Collection; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 50 /** 51 * Base fragment for dashboard style UI containing a list of static and dynamic setting items. 52 */ 53 public abstract class DashboardFragment extends SettingsPreferenceFragment 54 implements SettingsDrawerActivity.CategoryListener, Indexable, 55 SummaryLoader.SummaryConsumer { 56 private static final String TAG = "DashboardFragment"; 57 58 private final Map<Class, List<AbstractPreferenceController>> mPreferenceControllers = 59 new ArrayMap<>(); 60 private final Set<String> mDashboardTilePrefKeys = new ArraySet<>(); 61 62 private DashboardFeatureProvider mDashboardFeatureProvider; 63 private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController; 64 private boolean mListeningToCategoryChange; 65 private SummaryLoader mSummaryLoader; 66 67 @Override onAttach(Context context)68 public void onAttach(Context context) { 69 super.onAttach(context); 70 mDashboardFeatureProvider = FeatureFactory.getFactory(context). 71 getDashboardFeatureProvider(context); 72 final List<AbstractPreferenceController> controllers = new ArrayList<>(); 73 // Load preference controllers from code 74 final List<AbstractPreferenceController> controllersFromCode = 75 createPreferenceControllers(context); 76 // Load preference controllers from xml definition 77 final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper 78 .getPreferenceControllersFromXml(context, getPreferenceScreenResId()); 79 // Filter xml-based controllers in case a similar controller is created from code already. 80 final List<BasePreferenceController> uniqueControllerFromXml = 81 PreferenceControllerListHelper.filterControllers( 82 controllersFromXml, controllersFromCode); 83 84 // Add unique controllers to list. 85 if (controllersFromCode != null) { 86 controllers.addAll(controllersFromCode); 87 } 88 controllers.addAll(uniqueControllerFromXml); 89 90 // And wire up with lifecycle. 91 final Lifecycle lifecycle = getLifecycle(); 92 uniqueControllerFromXml 93 .stream() 94 .filter(controller -> controller instanceof LifecycleObserver) 95 .forEach( 96 controller -> lifecycle.addObserver((LifecycleObserver) controller)); 97 98 mPlaceholderPreferenceController = 99 new DashboardTilePlaceholderPreferenceController(context); 100 controllers.add(mPlaceholderPreferenceController); 101 for (AbstractPreferenceController controller : controllers) { 102 addPreferenceController(controller); 103 } 104 } 105 106 @Override onCreate(Bundle icicle)107 public void onCreate(Bundle icicle) { 108 super.onCreate(icicle); 109 // Set ComparisonCallback so we get better animation when list changes. 110 getPreferenceManager().setPreferenceComparisonCallback( 111 new PreferenceManager.SimplePreferenceComparisonCallback()); 112 if (icicle != null) { 113 // Upon rotation configuration change we need to update preference states before any 114 // editing dialog is recreated (that would happen before onResume is called). 115 updatePreferenceStates(); 116 } 117 } 118 119 @Override onCategoriesChanged()120 public void onCategoriesChanged() { 121 final DashboardCategory category = 122 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey()); 123 if (category == null) { 124 return; 125 } 126 refreshDashboardTiles(getLogTag()); 127 } 128 129 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)130 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 131 refreshAllPreferences(getLogTag()); 132 } 133 134 @Override onStart()135 public void onStart() { 136 super.onStart(); 137 final DashboardCategory category = 138 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey()); 139 if (category == null) { 140 return; 141 } 142 if (mSummaryLoader != null) { 143 // SummaryLoader can be null when there is no dynamic tiles. 144 mSummaryLoader.setListening(true); 145 } 146 final Activity activity = getActivity(); 147 if (activity instanceof SettingsDrawerActivity) { 148 mListeningToCategoryChange = true; 149 ((SettingsDrawerActivity) activity).addCategoryListener(this); 150 } 151 } 152 153 @Override notifySummaryChanged(Tile tile)154 public void notifySummaryChanged(Tile tile) { 155 final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); 156 final Preference pref = getPreferenceScreen().findPreference(key); 157 if (pref == null) { 158 Log.d(getLogTag(), 159 String.format("Can't find pref by key %s, skipping update summary %s/%s", 160 key, tile.title, tile.summary)); 161 return; 162 } 163 pref.setSummary(tile.summary); 164 } 165 166 @Override onResume()167 public void onResume() { 168 super.onResume(); 169 updatePreferenceStates(); 170 } 171 172 @Override onPreferenceTreeClick(Preference preference)173 public boolean onPreferenceTreeClick(Preference preference) { 174 Collection<List<AbstractPreferenceController>> controllers = 175 mPreferenceControllers.values(); 176 // If preference contains intent, log it before handling. 177 mMetricsFeatureProvider.logDashboardStartIntent( 178 getContext(), preference.getIntent(), getMetricsCategory()); 179 // Give all controllers a chance to handle click. 180 for (List<AbstractPreferenceController> controllerList : controllers) { 181 for (AbstractPreferenceController controller : controllerList) { 182 if (controller.handlePreferenceTreeClick(preference)) { 183 return true; 184 } 185 } 186 } 187 return super.onPreferenceTreeClick(preference); 188 } 189 190 @Override onStop()191 public void onStop() { 192 super.onStop(); 193 if (mSummaryLoader != null) { 194 // SummaryLoader can be null when there is no dynamic tiles. 195 mSummaryLoader.setListening(false); 196 } 197 if (mListeningToCategoryChange) { 198 final Activity activity = getActivity(); 199 if (activity instanceof SettingsDrawerActivity) { 200 ((SettingsDrawerActivity) activity).remCategoryListener(this); 201 } 202 mListeningToCategoryChange = false; 203 } 204 } 205 206 @Override getPreferenceScreenResId()207 protected abstract int getPreferenceScreenResId(); 208 use(Class<T> clazz)209 protected <T extends AbstractPreferenceController> T use(Class<T> clazz) { 210 List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz); 211 if (controllerList != null) { 212 if (controllerList.size() > 1) { 213 Log.w(TAG, "Multiple controllers of Class " + clazz.getSimpleName() 214 + " found, returning first one."); 215 } 216 return (T) controllerList.get(0); 217 } 218 219 return null; 220 } 221 addPreferenceController(AbstractPreferenceController controller)222 protected void addPreferenceController(AbstractPreferenceController controller) { 223 if (mPreferenceControllers.get(controller.getClass()) == null) { 224 mPreferenceControllers.put(controller.getClass(), new ArrayList<>()); 225 } 226 mPreferenceControllers.get(controller.getClass()).add(controller); 227 } 228 229 /** 230 * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment. 231 */ 232 @VisibleForTesting getCategoryKey()233 public String getCategoryKey() { 234 return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName()); 235 } 236 237 /** 238 * Get the tag string for logging. 239 */ getLogTag()240 protected abstract String getLogTag(); 241 242 /** 243 * Get a list of {@link AbstractPreferenceController} for this fragment. 244 */ createPreferenceControllers(Context context)245 protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { 246 return null; 247 } 248 249 /** 250 * Returns true if this tile should be displayed 251 */ displayTile(Tile tile)252 protected boolean displayTile(Tile tile) { 253 return true; 254 } 255 256 @VisibleForTesting tintTileIcon(Tile tile)257 boolean tintTileIcon(Tile tile) { 258 if (tile.icon == null) { 259 return false; 260 } 261 // First check if the tile has set the icon tintable metadata. 262 final Bundle metadata = tile.metaData; 263 if (metadata != null 264 && metadata.containsKey(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE)) { 265 return metadata.getBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE); 266 } 267 final String pkgName = getContext().getPackageName(); 268 // If this drawable is coming from outside Settings, tint it to match the color. 269 return pkgName != null && tile.intent != null 270 && !pkgName.equals(tile.intent.getComponent().getPackageName()); 271 } 272 273 /** 274 * Displays resource based tiles. 275 */ displayResourceTiles()276 private void displayResourceTiles() { 277 final int resId = getPreferenceScreenResId(); 278 if (resId <= 0) { 279 return; 280 } 281 addPreferencesFromResource(resId); 282 final PreferenceScreen screen = getPreferenceScreen(); 283 mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach( 284 controller -> controller.displayPreference(screen)); 285 } 286 287 /** 288 * Update state of each preference managed by PreferenceController. 289 */ updatePreferenceStates()290 protected void updatePreferenceStates() { 291 final PreferenceScreen screen = getPreferenceScreen(); 292 Collection<List<AbstractPreferenceController>> controllerLists = 293 mPreferenceControllers.values(); 294 for (List<AbstractPreferenceController> controllerList : controllerLists) { 295 for (AbstractPreferenceController controller : controllerList) { 296 if (!controller.isAvailable()) { 297 continue; 298 } 299 final String key = controller.getPreferenceKey(); 300 301 final Preference preference = screen.findPreference(key); 302 if (preference == null) { 303 Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s", 304 key, controller.getClass().getSimpleName())); 305 continue; 306 } 307 controller.updateState(preference); 308 } 309 } 310 } 311 312 /** 313 * Refresh all preference items, including both static prefs from xml, and dynamic items from 314 * DashboardCategory. 315 */ refreshAllPreferences(final String TAG)316 private void refreshAllPreferences(final String TAG) { 317 // First remove old preferences. 318 if (getPreferenceScreen() != null) { 319 // Intentionally do not cache PreferenceScreen because it will be recreated later. 320 getPreferenceScreen().removeAll(); 321 } 322 323 // Add resource based tiles. 324 displayResourceTiles(); 325 326 refreshDashboardTiles(TAG); 327 } 328 329 /** 330 * Refresh preference items backed by DashboardCategory. 331 */ 332 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) refreshDashboardTiles(final String TAG)333 void refreshDashboardTiles(final String TAG) { 334 final PreferenceScreen screen = getPreferenceScreen(); 335 336 final DashboardCategory category = 337 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey()); 338 if (category == null) { 339 Log.d(TAG, "NO dashboard tiles for " + TAG); 340 return; 341 } 342 final List<Tile> tiles = category.getTiles(); 343 if (tiles == null) { 344 Log.d(TAG, "tile list is empty, skipping category " + category.title); 345 return; 346 } 347 // Create a list to track which tiles are to be removed. 348 final List<String> remove = new ArrayList<>(mDashboardTilePrefKeys); 349 350 // There are dashboard tiles, so we need to install SummaryLoader. 351 if (mSummaryLoader != null) { 352 mSummaryLoader.release(); 353 } 354 final Context context = getContext(); 355 mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey()); 356 mSummaryLoader.setSummaryConsumer(this); 357 final TypedArray a = context.obtainStyledAttributes(new int[] { 358 android.R.attr.colorControlNormal}); 359 final int tintColor = a.getColor(0, context.getColor(android.R.color.white)); 360 a.recycle(); 361 // Install dashboard tiles. 362 for (Tile tile : tiles) { 363 final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); 364 if (TextUtils.isEmpty(key)) { 365 Log.d(TAG, "tile does not contain a key, skipping " + tile); 366 continue; 367 } 368 if (!displayTile(tile)) { 369 continue; 370 } 371 if (tintTileIcon(tile)) { 372 tile.icon.setTint(tintColor); 373 } 374 if (mDashboardTilePrefKeys.contains(key)) { 375 // Have the key already, will rebind. 376 final Preference preference = screen.findPreference(key); 377 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), getMetricsCategory(), 378 preference, tile, key, mPlaceholderPreferenceController.getOrder()); 379 } else { 380 // Don't have this key, add it. 381 final Preference pref = new Preference(getPrefContext()); 382 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), getMetricsCategory(), 383 pref, tile, key, mPlaceholderPreferenceController.getOrder()); 384 screen.addPreference(pref); 385 mDashboardTilePrefKeys.add(key); 386 } 387 remove.remove(key); 388 } 389 // Finally remove tiles that are gone. 390 for (String key : remove) { 391 mDashboardTilePrefKeys.remove(key); 392 final Preference preference = screen.findPreference(key); 393 if (preference != null) { 394 screen.removePreference(preference); 395 } 396 } 397 mSummaryLoader.setListening(true); 398 } 399 } 400