• 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.app.settings.SettingsEnums;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.os.Bundle;
23 import android.text.TextUtils;
24 import android.util.ArrayMap;
25 import android.util.Log;
26 
27 import androidx.annotation.CallSuper;
28 import androidx.annotation.VisibleForTesting;
29 import androidx.preference.Preference;
30 import androidx.preference.PreferenceGroup;
31 import androidx.preference.PreferenceManager;
32 import androidx.preference.PreferenceScreen;
33 import androidx.preference.SwitchPreference;
34 
35 import com.android.settings.R;
36 import com.android.settings.SettingsPreferenceFragment;
37 import com.android.settings.core.BasePreferenceController;
38 import com.android.settings.core.CategoryMixin.CategoryHandler;
39 import com.android.settings.core.CategoryMixin.CategoryListener;
40 import com.android.settings.core.PreferenceControllerListHelper;
41 import com.android.settings.overlay.FeatureFactory;
42 import com.android.settings.widget.PrimarySwitchPreference;
43 import com.android.settingslib.core.AbstractPreferenceController;
44 import com.android.settingslib.core.lifecycle.Lifecycle;
45 import com.android.settingslib.core.lifecycle.LifecycleObserver;
46 import com.android.settingslib.drawer.DashboardCategory;
47 import com.android.settingslib.drawer.ProviderTile;
48 import com.android.settingslib.drawer.Tile;
49 import com.android.settingslib.search.Indexable;
50 import com.android.settingslib.utils.ThreadUtils;
51 
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collection;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Objects;
58 import java.util.Set;
59 import java.util.concurrent.ExecutionException;
60 
61 /**
62  * Base fragment for dashboard style UI containing a list of static and dynamic setting items.
63  */
64 public abstract class DashboardFragment extends SettingsPreferenceFragment
65         implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
66         BasePreferenceController.UiBlockListener {
67     public static final String CATEGORY = "category";
68     private static final String TAG = "DashboardFragment";
69 
70     @VisibleForTesting
71     final ArrayMap<String, List<DynamicDataObserver>> mDashboardTilePrefKeys = new ArrayMap<>();
72     private final Map<Class, List<AbstractPreferenceController>> mPreferenceControllers =
73             new ArrayMap<>();
74     private final List<DynamicDataObserver> mRegisteredObservers = new ArrayList<>();
75     private final List<AbstractPreferenceController> mControllers = new ArrayList<>();
76     @VisibleForTesting
77     UiBlockerController mBlockerController;
78     private DashboardFeatureProvider mDashboardFeatureProvider;
79     private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController;
80     private boolean mListeningToCategoryChange;
81     private List<String> mSuppressInjectedTileKeys;
82 
83     @Override
onAttach(Context context)84     public void onAttach(Context context) {
85         super.onAttach(context);
86         mSuppressInjectedTileKeys = Arrays.asList(context.getResources().getStringArray(
87                 R.array.config_suppress_injected_tile_keys));
88         mDashboardFeatureProvider = FeatureFactory.getFactory(context).
89                 getDashboardFeatureProvider(context);
90         // Load preference controllers from code
91         final List<AbstractPreferenceController> controllersFromCode =
92                 createPreferenceControllers(context);
93         // Load preference controllers from xml definition
94         final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper
95                 .getPreferenceControllersFromXml(context, getPreferenceScreenResId());
96         // Filter xml-based controllers in case a similar controller is created from code already.
97         final List<BasePreferenceController> uniqueControllerFromXml =
98                 PreferenceControllerListHelper.filterControllers(
99                         controllersFromXml, controllersFromCode);
100 
101         // Add unique controllers to list.
102         if (controllersFromCode != null) {
103             mControllers.addAll(controllersFromCode);
104         }
105         mControllers.addAll(uniqueControllerFromXml);
106 
107         // And wire up with lifecycle.
108         final Lifecycle lifecycle = getSettingsLifecycle();
109         uniqueControllerFromXml.forEach(controller -> {
110             if (controller instanceof LifecycleObserver) {
111                 lifecycle.addObserver((LifecycleObserver) controller);
112             }
113         });
114 
115         // Set metrics category for BasePreferenceController.
116         final int metricCategory = getMetricsCategory();
117         mControllers.forEach(controller -> {
118             if (controller instanceof BasePreferenceController) {
119                 ((BasePreferenceController) controller).setMetricsCategory(metricCategory);
120             }
121         });
122 
123         mPlaceholderPreferenceController =
124                 new DashboardTilePlaceholderPreferenceController(context);
125         mControllers.add(mPlaceholderPreferenceController);
126         for (AbstractPreferenceController controller : mControllers) {
127             addPreferenceController(controller);
128         }
129     }
130 
131     @VisibleForTesting
checkUiBlocker(List<AbstractPreferenceController> controllers)132     void checkUiBlocker(List<AbstractPreferenceController> controllers) {
133         final List<String> keys = new ArrayList<>();
134         controllers.forEach(controller -> {
135             if (controller instanceof BasePreferenceController.UiBlocker
136                     && controller.isAvailable()) {
137                 ((BasePreferenceController) controller).setUiBlockListener(this);
138                 keys.add(controller.getPreferenceKey());
139             }
140         });
141 
142         if (!keys.isEmpty()) {
143             mBlockerController = new UiBlockerController(keys);
144             mBlockerController.start(() -> updatePreferenceVisibility(mPreferenceControllers));
145         }
146     }
147 
148     @Override
onCreate(Bundle icicle)149     public void onCreate(Bundle icicle) {
150         super.onCreate(icicle);
151         // Set ComparisonCallback so we get better animation when list changes.
152         getPreferenceManager().setPreferenceComparisonCallback(
153                 new PreferenceManager.SimplePreferenceComparisonCallback());
154         if (icicle != null) {
155             // Upon rotation configuration change we need to update preference states before any
156             // editing dialog is recreated (that would happen before onResume is called).
157             updatePreferenceStates();
158         }
159     }
160 
161     @Override
onCategoriesChanged(Set<String> categories)162     public void onCategoriesChanged(Set<String> categories) {
163         final String categoryKey = getCategoryKey();
164         final DashboardCategory dashboardCategory =
165                 mDashboardFeatureProvider.getTilesForCategory(categoryKey);
166         if (dashboardCategory == null) {
167             return;
168         }
169 
170         if (categories == null) {
171             // force refreshing
172             refreshDashboardTiles(getLogTag());
173         } else if (categories.contains(categoryKey)) {
174             Log.i(TAG, "refresh tiles for " + categoryKey);
175             refreshDashboardTiles(getLogTag());
176         }
177     }
178 
179     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)180     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
181         checkUiBlocker(mControllers);
182         refreshAllPreferences(getLogTag());
183         mControllers.stream()
184                 .map(controller -> (Preference) findPreference(controller.getPreferenceKey()))
185                 .filter(Objects::nonNull)
186                 .forEach(preference -> {
187                     // Give all controllers a chance to handle click.
188                     preference.getExtras().putInt(CATEGORY, getMetricsCategory());
189                 });
190     }
191 
192     @Override
onStart()193     public void onStart() {
194         super.onStart();
195         final DashboardCategory category =
196                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
197         if (category == null) {
198             return;
199         }
200         final Activity activity = getActivity();
201         if (activity instanceof CategoryHandler) {
202             mListeningToCategoryChange = true;
203             ((CategoryHandler) activity).getCategoryMixin().addCategoryListener(this);
204         }
205         final ContentResolver resolver = getContentResolver();
206         mDashboardTilePrefKeys.values().stream()
207                 .filter(Objects::nonNull)
208                 .flatMap(List::stream)
209                 .forEach(observer -> {
210                     if (!mRegisteredObservers.contains(observer)) {
211                         registerDynamicDataObserver(resolver, observer);
212                     }
213                 });
214     }
215 
216     @Override
onResume()217     public void onResume() {
218         super.onResume();
219         updatePreferenceStates();
220         writeElapsedTimeMetric(SettingsEnums.ACTION_DASHBOARD_VISIBLE_TIME,
221                 "isParalleledControllers:" + isParalleledControllers());
222     }
223 
224     @Override
onPreferenceTreeClick(Preference preference)225     public boolean onPreferenceTreeClick(Preference preference) {
226         final Collection<List<AbstractPreferenceController>> controllers =
227                 mPreferenceControllers.values();
228         for (List<AbstractPreferenceController> controllerList : controllers) {
229             for (AbstractPreferenceController controller : controllerList) {
230                 if (controller.handlePreferenceTreeClick(preference)) {
231                     // log here since calling super.onPreferenceTreeClick will be skipped
232                     writePreferenceClickMetric(preference);
233                     return true;
234                 }
235             }
236         }
237         return super.onPreferenceTreeClick(preference);
238     }
239 
240     @Override
onStop()241     public void onStop() {
242         super.onStop();
243         unregisterDynamicDataObservers(new ArrayList<>(mRegisteredObservers));
244         if (mListeningToCategoryChange) {
245             final Activity activity = getActivity();
246             if (activity instanceof CategoryHandler) {
247                 ((CategoryHandler) activity).getCategoryMixin().removeCategoryListener(this);
248             }
249             mListeningToCategoryChange = false;
250         }
251     }
252 
253     @Override
getPreferenceScreenResId()254     protected abstract int getPreferenceScreenResId();
255 
256     @Override
onExpandButtonClick()257     public void onExpandButtonClick() {
258         mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
259                 SettingsEnums.ACTION_SETTINGS_ADVANCED_BUTTON_EXPAND,
260                 getMetricsCategory(), null, 0);
261     }
262 
shouldForceRoundedIcon()263     protected boolean shouldForceRoundedIcon() {
264         return false;
265     }
266 
use(Class<T> clazz)267     protected <T extends AbstractPreferenceController> T use(Class<T> clazz) {
268         List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz);
269         if (controllerList != null) {
270             if (controllerList.size() > 1) {
271                 Log.w(TAG, "Multiple controllers of Class " + clazz.getSimpleName()
272                         + " found, returning first one.");
273             }
274             return (T) controllerList.get(0);
275         }
276 
277         return null;
278     }
279 
addPreferenceController(AbstractPreferenceController controller)280     protected void addPreferenceController(AbstractPreferenceController controller) {
281         if (mPreferenceControllers.get(controller.getClass()) == null) {
282             mPreferenceControllers.put(controller.getClass(), new ArrayList<>());
283         }
284         mPreferenceControllers.get(controller.getClass()).add(controller);
285     }
286 
287     /**
288      * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment.
289      */
290     @VisibleForTesting
getCategoryKey()291     public String getCategoryKey() {
292         return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
293     }
294 
295     /**
296      * Get the tag string for logging.
297      */
getLogTag()298     protected abstract String getLogTag();
299 
300     /**
301      * Get a list of {@link AbstractPreferenceController} for this fragment.
302      */
createPreferenceControllers(Context context)303     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
304         return null;
305     }
306 
307     /**
308      * Returns true if this tile should be displayed
309      */
310     @CallSuper
displayTile(Tile tile)311     protected boolean displayTile(Tile tile) {
312         if (mSuppressInjectedTileKeys != null && tile.hasKey()) {
313             // For suppressing injected tiles for OEMs.
314             return !mSuppressInjectedTileKeys.contains(tile.getKey(getContext()));
315         }
316         return true;
317     }
318 
319     /**
320      * Displays resource based tiles.
321      */
displayResourceTiles()322     private void displayResourceTiles() {
323         final int resId = getPreferenceScreenResId();
324         if (resId <= 0) {
325             return;
326         }
327         addPreferencesFromResource(resId);
328         final PreferenceScreen screen = getPreferenceScreen();
329         screen.setOnExpandButtonClickListener(this);
330         displayResourceTilesToScreen(screen);
331     }
332 
333     /**
334      * Perform {@link AbstractPreferenceController#displayPreference(PreferenceScreen)}
335      * on all {@link AbstractPreferenceController}s.
336      */
displayResourceTilesToScreen(PreferenceScreen screen)337     protected void displayResourceTilesToScreen(PreferenceScreen screen) {
338         mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach(
339                 controller -> controller.displayPreference(screen));
340     }
341 
342     /**
343      * @return {@code true} if the underlying controllers should be executed in parallel.
344      * Override this function to enable/disable the behavior.
345      */
isParalleledControllers()346     protected boolean isParalleledControllers() {
347         return false;
348     }
349 
350     /**
351      * Get current PreferenceController(s)
352      */
getPreferenceControllers()353     protected Collection<List<AbstractPreferenceController>> getPreferenceControllers() {
354         return mPreferenceControllers.values();
355     }
356 
357     /**
358      * Update state of each preference managed by PreferenceController.
359      */
updatePreferenceStates()360     protected void updatePreferenceStates() {
361         final PreferenceScreen screen = getPreferenceScreen();
362         Collection<List<AbstractPreferenceController>> controllerLists =
363                 mPreferenceControllers.values();
364         for (List<AbstractPreferenceController> controllerList : controllerLists) {
365             for (AbstractPreferenceController controller : controllerList) {
366                 if (!controller.isAvailable()) {
367                     continue;
368                 }
369 
370                 final String key = controller.getPreferenceKey();
371                 if (TextUtils.isEmpty(key)) {
372                     Log.d(TAG, String.format("Preference key is %s in Controller %s",
373                             key, controller.getClass().getSimpleName()));
374                     continue;
375                 }
376 
377                 final Preference preference = screen.findPreference(key);
378                 if (preference == null) {
379                     Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s",
380                             key, controller.getClass().getSimpleName()));
381                     continue;
382                 }
383                 controller.updateState(preference);
384             }
385         }
386     }
387 
388     /**
389      * Use parallel method to update state of each preference managed by PreferenceController.
390      */
391     @VisibleForTesting
392     // To use this parallel approach will cause the side effect of the UI flicker. Such as
393     // the thumb sliding of the toggle button.
updatePreferenceStatesInParallel()394     void updatePreferenceStatesInParallel() {
395         final PreferenceScreen screen = getPreferenceScreen();
396         final Collection<List<AbstractPreferenceController>> controllerLists =
397                 mPreferenceControllers.values();
398         final List<ControllerFutureTask> taskList = new ArrayList<>();
399         for (List<AbstractPreferenceController> controllerList : controllerLists) {
400             for (AbstractPreferenceController controller : controllerList) {
401                 final ControllerFutureTask task = new ControllerFutureTask(
402                         new ControllerTask(controller, screen, mMetricsFeatureProvider,
403                                 getMetricsCategory()), null /* result */);
404                 taskList.add(task);
405                 ThreadUtils.postOnBackgroundThread(task);
406             }
407         }
408 
409         for (ControllerFutureTask task : taskList) {
410             try {
411                 task.get();
412             } catch (InterruptedException | ExecutionException e) {
413                 Log.w(TAG, task.getController().getPreferenceKey() + " " + e.getMessage());
414             }
415         }
416     }
417 
418     /**
419      * Refresh all preference items, including both static prefs from xml, and dynamic items from
420      * DashboardCategory.
421      */
refreshAllPreferences(final String tag)422     private void refreshAllPreferences(final String tag) {
423         final PreferenceScreen screen = getPreferenceScreen();
424         // First remove old preferences.
425         if (screen != null) {
426             // Intentionally do not cache PreferenceScreen because it will be recreated later.
427             screen.removeAll();
428         }
429 
430         // Add resource based tiles.
431         displayResourceTiles();
432 
433         refreshDashboardTiles(tag);
434 
435         final Activity activity = getActivity();
436         if (activity != null) {
437             Log.d(tag, "All preferences added, reporting fully drawn");
438             activity.reportFullyDrawn();
439         }
440 
441         updatePreferenceVisibility(mPreferenceControllers);
442     }
443 
444     @VisibleForTesting
updatePreferenceVisibility( Map<Class, List<AbstractPreferenceController>> preferenceControllers)445     void updatePreferenceVisibility(
446             Map<Class, List<AbstractPreferenceController>> preferenceControllers) {
447         final PreferenceScreen screen = getPreferenceScreen();
448         if (screen == null || preferenceControllers == null || mBlockerController == null) {
449             return;
450         }
451 
452         final boolean visible = mBlockerController.isBlockerFinished();
453         for (List<AbstractPreferenceController> controllerList :
454                 preferenceControllers.values()) {
455             for (AbstractPreferenceController controller : controllerList) {
456                 final String key = controller.getPreferenceKey();
457                 final Preference preference = findPreference(key);
458                 if (preference != null) {
459                     preference.setVisible(visible && controller.isAvailable());
460                 }
461             }
462         }
463     }
464 
465     /**
466      * Refresh preference items backed by DashboardCategory.
467      */
refreshDashboardTiles(final String tag)468     private void refreshDashboardTiles(final String tag) {
469         final PreferenceScreen screen = getPreferenceScreen();
470 
471         final DashboardCategory category =
472                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
473         if (category == null) {
474             Log.d(tag, "NO dashboard tiles for " + tag);
475             return;
476         }
477         final List<Tile> tiles = category.getTiles();
478         if (tiles == null) {
479             Log.d(tag, "tile list is empty, skipping category " + category.key);
480             return;
481         }
482         // Create a list to track which tiles are to be removed.
483         final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
484 
485         // Install dashboard tiles.
486         final boolean forceRoundedIcons = shouldForceRoundedIcon();
487         for (Tile tile : tiles) {
488             final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
489             if (TextUtils.isEmpty(key)) {
490                 Log.d(tag, "tile does not contain a key, skipping " + tile);
491                 continue;
492             }
493             if (!displayTile(tile)) {
494                 continue;
495             }
496             if (mDashboardTilePrefKeys.containsKey(key)) {
497                 // Have the key already, will rebind.
498                 final Preference preference = screen.findPreference(key);
499                 mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),
500                         forceRoundedIcons, getMetricsCategory(), preference, tile, key,
501                         mPlaceholderPreferenceController.getOrder());
502             } else {
503                 // Don't have this key, add it.
504                 final Preference pref = createPreference(tile);
505                 final List<DynamicDataObserver> observers =
506                         mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),
507                                 forceRoundedIcons, getMetricsCategory(), pref, tile, key,
508                                 mPlaceholderPreferenceController.getOrder());
509                 screen.addPreference(pref);
510                 registerDynamicDataObservers(observers);
511                 mDashboardTilePrefKeys.put(key, observers);
512             }
513             remove.remove(key);
514         }
515         // Finally remove tiles that are gone.
516         for (Map.Entry<String, List<DynamicDataObserver>> entry : remove.entrySet()) {
517             final String key = entry.getKey();
518             mDashboardTilePrefKeys.remove(key);
519             final Preference preference = screen.findPreference(key);
520             if (preference != null) {
521                 screen.removePreference(preference);
522             }
523             unregisterDynamicDataObservers(entry.getValue());
524         }
525     }
526 
527     @Override
onBlockerWorkFinished(BasePreferenceController controller)528     public void onBlockerWorkFinished(BasePreferenceController controller) {
529         mBlockerController.countDown(controller.getPreferenceKey());
530     }
531 
532     @VisibleForTesting
createPreference(Tile tile)533     Preference createPreference(Tile tile) {
534         return tile instanceof ProviderTile
535                 ? new SwitchPreference(getPrefContext())
536                 : tile.hasSwitch()
537                         ? new PrimarySwitchPreference(getPrefContext())
538                         : new Preference(getPrefContext());
539     }
540 
541     @VisibleForTesting
registerDynamicDataObservers(List<DynamicDataObserver> observers)542     void registerDynamicDataObservers(List<DynamicDataObserver> observers) {
543         if (observers == null || observers.isEmpty()) {
544             return;
545         }
546         final ContentResolver resolver = getContentResolver();
547         observers.forEach(observer -> registerDynamicDataObserver(resolver, observer));
548     }
549 
registerDynamicDataObserver(ContentResolver resolver, DynamicDataObserver observer)550     private void registerDynamicDataObserver(ContentResolver resolver,
551             DynamicDataObserver observer) {
552         Log.d(TAG, "register observer: @" + Integer.toHexString(observer.hashCode())
553                 + ", uri: " + observer.getUri());
554         resolver.registerContentObserver(observer.getUri(), false, observer);
555         mRegisteredObservers.add(observer);
556     }
557 
unregisterDynamicDataObservers(List<DynamicDataObserver> observers)558     private void unregisterDynamicDataObservers(List<DynamicDataObserver> observers) {
559         if (observers == null || observers.isEmpty()) {
560             return;
561         }
562         final ContentResolver resolver = getContentResolver();
563         observers.forEach(observer -> {
564             Log.d(TAG, "unregister observer: @" + Integer.toHexString(observer.hashCode())
565                     + ", uri: " + observer.getUri());
566             mRegisteredObservers.remove(observer);
567             resolver.unregisterContentObserver(observer);
568         });
569     }
570 }
571