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