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