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