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