1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settings.dashboard;
17 
18 import android.app.Activity;
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Bundle;
22 import android.support.annotation.VisibleForTesting;
23 import android.support.v7.preference.Preference;
24 import android.support.v7.preference.PreferenceManager;
25 import android.support.v7.preference.PreferenceScreen;
26 import android.text.TextUtils;
27 import android.util.ArrayMap;
28 import android.util.ArraySet;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 
34 import com.android.settings.SettingsPreferenceFragment;
35 import com.android.settings.core.PreferenceController;
36 import com.android.settings.overlay.FeatureFactory;
37 import com.android.settings.search.Indexable;
38 import com.android.settingslib.drawer.DashboardCategory;
39 import com.android.settingslib.drawer.SettingsDrawerActivity;
40 import com.android.settingslib.drawer.Tile;
41 
42 import java.util.ArrayList;
43 import java.util.Collection;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Set;
47 
48 /**
49  * Base fragment for dashboard style UI containing a list of static and dynamic setting items.
50  */
51 public abstract class DashboardFragment extends SettingsPreferenceFragment
52         implements SettingsDrawerActivity.CategoryListener, Indexable,
53         SummaryLoader.SummaryConsumer {
54     private static final String TAG = "DashboardFragment";
55 
56     private final Map<Class, PreferenceController> mPreferenceControllers =
57             new ArrayMap<>();
58     private final Set<String> mDashboardTilePrefKeys = new ArraySet<>();
59 
60     protected ProgressiveDisclosureMixin mProgressiveDisclosureMixin;
61     protected DashboardFeatureProvider mDashboardFeatureProvider;
62     private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController;
63     private boolean mListeningToCategoryChange;
64     private SummaryLoader mSummaryLoader;
65 
66     @Override
onAttach(Context context)67     public void onAttach(Context context) {
68         super.onAttach(context);
69         mDashboardFeatureProvider =
70                 FeatureFactory.getFactory(context).getDashboardFeatureProvider(context);
71         mProgressiveDisclosureMixin = mDashboardFeatureProvider
72                 .getProgressiveDisclosureMixin(context, this, getArguments());
73         getLifecycle().addObserver(mProgressiveDisclosureMixin);
74 
75         List<PreferenceController> controllers = getPreferenceControllers(context);
76         if (controllers == null) {
77             controllers = new ArrayList<>();
78         }
79         mPlaceholderPreferenceController =
80                 new DashboardTilePlaceholderPreferenceController(context);
81         controllers.add(mPlaceholderPreferenceController);
82         for (PreferenceController controller : controllers) {
83             addPreferenceController(controller);
84         }
85     }
86 
87     @Override
onCreate(Bundle icicle)88     public void onCreate(Bundle icicle) {
89         super.onCreate(icicle);
90         // Set ComparisonCallback so we get better animation when list changes.
91         getPreferenceManager().setPreferenceComparisonCallback(
92                 new PreferenceManager.SimplePreferenceComparisonCallback());
93     }
94 
95     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)96     public View onCreateView(LayoutInflater inflater, ViewGroup container,
97             Bundle savedInstanceState) {
98         final View view = super.onCreateView(inflater, container, savedInstanceState);
99         return view;
100     }
101 
102     @Override
onCategoriesChanged()103     public void onCategoriesChanged() {
104         final DashboardCategory category =
105                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
106         if (category == null) {
107             return;
108         }
109         refreshDashboardTiles(getLogTag());
110     }
111 
112     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)113     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
114         super.onCreatePreferences(savedInstanceState, rootKey);
115         refreshAllPreferences(getLogTag());
116     }
117 
118     @Override
onStart()119     public void onStart() {
120         super.onStart();
121         final DashboardCategory category =
122                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
123         if (category == null) {
124             return;
125         }
126         if (mSummaryLoader != null) {
127             // SummaryLoader can be null when there is no dynamic tiles.
128             mSummaryLoader.setListening(true);
129         }
130         final Activity activity = getActivity();
131         if (activity instanceof SettingsDrawerActivity) {
132             mListeningToCategoryChange = true;
133             ((SettingsDrawerActivity) activity).addCategoryListener(this);
134         }
135     }
136 
137     @Override
notifySummaryChanged(Tile tile)138     public void notifySummaryChanged(Tile tile) {
139         final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
140         final Preference pref = mProgressiveDisclosureMixin.findPreference(
141                 getPreferenceScreen(), key);
142         if (pref == null) {
143             Log.d(getLogTag(),
144                     String.format("Can't find pref by key %s, skipping update summary %s/%s",
145                             key, tile.title, tile.summary));
146             return;
147         }
148         pref.setSummary(tile.summary);
149     }
150 
151     @Override
onResume()152     public void onResume() {
153         super.onResume();
154         updatePreferenceStates();
155     }
156 
157     @Override
onPreferenceTreeClick(Preference preference)158     public boolean onPreferenceTreeClick(Preference preference) {
159         Collection<PreferenceController> controllers = mPreferenceControllers.values();
160         // If preference contains intent, log it before handling.
161         mMetricsFeatureProvider.logDashboardStartIntent(
162                 getContext(), preference.getIntent(), getMetricsCategory());
163         // Give all controllers a chance to handle click.
164         for (PreferenceController controller : controllers) {
165             if (controller.handlePreferenceTreeClick(preference)) {
166                 return true;
167             }
168         }
169         return super.onPreferenceTreeClick(preference);
170     }
171 
172     @Override
onStop()173     public void onStop() {
174         super.onStop();
175         if (mSummaryLoader != null) {
176             // SummaryLoader can be null when there is no dynamic tiles.
177             mSummaryLoader.setListening(false);
178         }
179         if (mListeningToCategoryChange) {
180             final Activity activity = getActivity();
181             if (activity instanceof SettingsDrawerActivity) {
182                 ((SettingsDrawerActivity) activity).remCategoryListener(this);
183             }
184             mListeningToCategoryChange = false;
185         }
186     }
187 
getPreferenceController(Class<T> clazz)188     protected <T extends PreferenceController> T getPreferenceController(Class<T> clazz) {
189         PreferenceController controller = mPreferenceControllers.get(clazz);
190         return (T) controller;
191     }
192 
addPreferenceController(PreferenceController controller)193     protected void addPreferenceController(PreferenceController controller) {
194         mPreferenceControllers.put(controller.getClass(), controller);
195     }
196 
197     /**
198      * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment.
199      */
200     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
getCategoryKey()201     public String getCategoryKey() {
202         return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
203     }
204 
205     /**
206      * Get the tag string for logging.
207      */
getLogTag()208     protected abstract String getLogTag();
209 
210     /**
211      * Get the res id for static preference xml for this fragment.
212      */
getPreferenceScreenResId()213     protected abstract int getPreferenceScreenResId();
214 
215     /**
216      * Get a list of {@link PreferenceController} for this fragment.
217      */
getPreferenceControllers(Context context)218     protected abstract List<PreferenceController> getPreferenceControllers(Context context);
219 
220     /**
221      * Returns true if this tile should be displayed
222      */
displayTile(Tile tile)223     protected boolean displayTile(Tile tile) {
224         return true;
225     }
226 
227     /**
228      * Displays resource based tiles.
229      */
displayResourceTiles()230     private void displayResourceTiles() {
231         final int resId = getPreferenceScreenResId();
232         if (resId <= 0) {
233             return;
234         }
235         addPreferencesFromResource(resId);
236         final PreferenceScreen screen = getPreferenceScreen();
237         Collection<PreferenceController> controllers = mPreferenceControllers.values();
238         for (PreferenceController controller : controllers) {
239             controller.displayPreference(screen);
240         }
241     }
242 
243     /**
244      * Update state of each preference managed by PreferenceController.
245      */
updatePreferenceStates()246     protected void updatePreferenceStates() {
247         Collection<PreferenceController> controllers = mPreferenceControllers.values();
248         final PreferenceScreen screen = getPreferenceScreen();
249         for (PreferenceController controller : controllers) {
250             if (!controller.isAvailable()) {
251                 continue;
252             }
253             final String key = controller.getPreferenceKey();
254 
255             final Preference preference = mProgressiveDisclosureMixin.findPreference(screen, key);
256             if (preference == null) {
257                 Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s",
258                         key, controller.getClass().getSimpleName()));
259                 continue;
260             }
261             controller.updateState(preference);
262         }
263     }
264 
265     /**
266      * Refresh all preference items, including both static prefs from xml, and dynamic items from
267      * DashboardCategory.
268      */
refreshAllPreferences(final String TAG)269     private void refreshAllPreferences(final String TAG) {
270         // First remove old preferences.
271         if (getPreferenceScreen() != null) {
272             // Intentionally do not cache PreferenceScreen because it will be recreated later.
273             getPreferenceScreen().removeAll();
274         }
275 
276         // Add resource based tiles.
277         displayResourceTiles();
278         mProgressiveDisclosureMixin.collapse(getPreferenceScreen());
279 
280         refreshDashboardTiles(TAG);
281     }
282 
283     /**
284      * Refresh preference items backed by DashboardCategory.
285      */
286     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
refreshDashboardTiles(final String TAG)287     void refreshDashboardTiles(final String TAG) {
288         final PreferenceScreen screen = getPreferenceScreen();
289 
290         final DashboardCategory category =
291                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
292         if (category == null) {
293             Log.d(TAG, "NO dashboard tiles for " + TAG);
294             return;
295         }
296         List<Tile> tiles = category.tiles;
297         if (tiles == null) {
298             Log.d(TAG, "tile list is empty, skipping category " + category.title);
299             return;
300         }
301         // Create a list to track which tiles are to be removed.
302         final List<String> remove = new ArrayList<>(mDashboardTilePrefKeys);
303 
304         // There are dashboard tiles, so we need to install SummaryLoader.
305         if (mSummaryLoader != null) {
306             mSummaryLoader.release();
307         }
308         final Context context = getContext();
309         mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey());
310         mSummaryLoader.setSummaryConsumer(this);
311         final TypedArray a = context.obtainStyledAttributes(new int[]{
312                 android.R.attr.colorControlNormal});
313         final int tintColor = a.getColor(0, context.getColor(android.R.color.white));
314         a.recycle();
315         final String pkgName = context.getPackageName();
316         // Install dashboard tiles.
317         for (Tile tile : tiles) {
318             final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
319             if (TextUtils.isEmpty(key)) {
320                 Log.d(TAG, "tile does not contain a key, skipping " + tile);
321                 continue;
322             }
323             if (!displayTile(tile)) {
324                 continue;
325             }
326             if (pkgName != null && tile.intent != null
327                     && !pkgName.equals(tile.intent.getComponent().getPackageName())) {
328                 // If this drawable is coming from outside Settings, tint it to match the color.
329                 tile.icon.setTint(tintColor);
330             }
331             if (mDashboardTilePrefKeys.contains(key)) {
332                 // Have the key already, will rebind.
333                 final Preference preference = mProgressiveDisclosureMixin.findPreference(
334                         screen, key);
335                 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), getMetricsCategory(),
336                         preference, tile, key, mPlaceholderPreferenceController.getOrder());
337             } else {
338                 // Don't have this key, add it.
339                 final Preference pref = new Preference(getPrefContext());
340                 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), getMetricsCategory(),
341                         pref, tile, key, mPlaceholderPreferenceController.getOrder());
342                 mProgressiveDisclosureMixin.addPreference(screen, pref);
343                 mDashboardTilePrefKeys.add(key);
344             }
345             remove.remove(key);
346         }
347         // Finally remove tiles that are gone.
348         for (String key : remove) {
349             mDashboardTilePrefKeys.remove(key);
350             mProgressiveDisclosureMixin.removePreference(screen, key);
351         }
352         mSummaryLoader.setListening(true);
353     }
354 }
355