• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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