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      * Get current PreferenceController(s)
318      */
getPreferenceControllers()319     protected Collection<List<AbstractPreferenceController>> getPreferenceControllers() {
320         return mPreferenceControllers.values();
321     }
322 
323     /**
324      * Update state of each preference managed by PreferenceController.
325      */
updatePreferenceStates()326     protected void updatePreferenceStates() {
327         final PreferenceScreen screen = getPreferenceScreen();
328         Collection<List<AbstractPreferenceController>> controllerLists =
329                 mPreferenceControllers.values();
330         for (List<AbstractPreferenceController> controllerList : controllerLists) {
331             for (AbstractPreferenceController controller : controllerList) {
332                 if (!controller.isAvailable()) {
333                     continue;
334                 }
335 
336                 final String key = controller.getPreferenceKey();
337                 if (TextUtils.isEmpty(key)) {
338                     Log.d(TAG, String.format("Preference key is %s in Controller %s",
339                             key, controller.getClass().getSimpleName()));
340                     continue;
341                 }
342 
343                 final Preference preference = screen.findPreference(key);
344                 if (preference == null) {
345                     Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s",
346                             key, controller.getClass().getSimpleName()));
347                     continue;
348                 }
349                 controller.updateState(preference);
350             }
351         }
352     }
353 
354     /**
355      * Refresh all preference items, including both static prefs from xml, and dynamic items from
356      * DashboardCategory.
357      */
refreshAllPreferences(final String TAG)358     private void refreshAllPreferences(final String TAG) {
359         final PreferenceScreen screen = getPreferenceScreen();
360         // First remove old preferences.
361         if (screen != null) {
362             // Intentionally do not cache PreferenceScreen because it will be recreated later.
363             screen.removeAll();
364         }
365 
366         // Add resource based tiles.
367         displayResourceTiles();
368 
369         refreshDashboardTiles(TAG);
370 
371         final Activity activity = getActivity();
372         if (activity != null) {
373             Log.d(TAG, "All preferences added, reporting fully drawn");
374             activity.reportFullyDrawn();
375         }
376 
377         updatePreferenceVisibility(mPreferenceControllers);
378     }
379 
380     @VisibleForTesting
updatePreferenceVisibility( Map<Class, List<AbstractPreferenceController>> preferenceControllers)381     void updatePreferenceVisibility(
382             Map<Class, List<AbstractPreferenceController>> preferenceControllers) {
383         final PreferenceScreen screen = getPreferenceScreen();
384         if (screen == null || preferenceControllers == null || mBlockerController == null) {
385             return;
386         }
387 
388         final boolean visible = mBlockerController.isBlockerFinished();
389         for (List<AbstractPreferenceController> controllerList :
390                 preferenceControllers.values()) {
391             for (AbstractPreferenceController controller : controllerList) {
392                 final String key = controller.getPreferenceKey();
393                 final Preference preference = findPreference(key);
394                 if (preference != null) {
395                     preference.setVisible(visible && controller.isAvailable());
396                 }
397             }
398         }
399     }
400 
401     /**
402      * Refresh preference items backed by DashboardCategory.
403      */
404     @VisibleForTesting
refreshDashboardTiles(final String TAG)405     void refreshDashboardTiles(final String TAG) {
406         final PreferenceScreen screen = getPreferenceScreen();
407 
408         final DashboardCategory category =
409                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
410         if (category == null) {
411             Log.d(TAG, "NO dashboard tiles for " + TAG);
412             return;
413         }
414         final List<Tile> tiles = category.getTiles();
415         if (tiles == null) {
416             Log.d(TAG, "tile list is empty, skipping category " + category.key);
417             return;
418         }
419         // Create a list to track which tiles are to be removed.
420         final List<String> remove = new ArrayList<>(mDashboardTilePrefKeys);
421 
422         // There are dashboard tiles, so we need to install SummaryLoader.
423         if (mSummaryLoader != null) {
424             mSummaryLoader.release();
425         }
426         final Context context = getContext();
427         mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey());
428         mSummaryLoader.setSummaryConsumer(this);
429         // Install dashboard tiles.
430         final boolean forceRoundedIcons = shouldForceRoundedIcon();
431         for (Tile tile : tiles) {
432             final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
433             if (TextUtils.isEmpty(key)) {
434                 Log.d(TAG, "tile does not contain a key, skipping " + tile);
435                 continue;
436             }
437             if (!displayTile(tile)) {
438                 continue;
439             }
440             if (mDashboardTilePrefKeys.contains(key)) {
441                 // Have the key already, will rebind.
442                 final Preference preference = screen.findPreference(key);
443                 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
444                         getMetricsCategory(), preference, tile, key,
445                         mPlaceholderPreferenceController.getOrder());
446             } else {
447                 // Don't have this key, add it.
448                 final Preference pref = new Preference(getPrefContext());
449                 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
450                         getMetricsCategory(), pref, tile, key,
451                         mPlaceholderPreferenceController.getOrder());
452                 screen.addPreference(pref);
453                 mDashboardTilePrefKeys.add(key);
454             }
455             remove.remove(key);
456         }
457         // Finally remove tiles that are gone.
458         for (String key : remove) {
459             mDashboardTilePrefKeys.remove(key);
460             final Preference preference = screen.findPreference(key);
461             if (preference != null) {
462                 screen.removePreference(preference);
463             }
464         }
465         mSummaryLoader.setListening(true);
466     }
467 
468     @Override
onBlockerWorkFinished(BasePreferenceController controller)469     public void onBlockerWorkFinished(BasePreferenceController controller) {
470         mBlockerController.countDown(controller.getPreferenceKey());
471     }
472 }
473