Home | History | Annotate | Download | only in settingslib
      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.settingslib;
     17 
     18 import android.content.Context;
     19 import android.content.Intent;
     20 import android.content.SharedPreferences;
     21 import android.os.UserHandle;
     22 import android.text.TextUtils;
     23 import android.util.ArrayMap;
     24 import android.util.AttributeSet;
     25 import android.util.Log;
     26 import android.util.Pair;
     27 import android.util.Xml;
     28 import android.provider.Settings;
     29 import android.accounts.Account;
     30 import android.accounts.AccountManager;
     31 import android.content.pm.PackageManager;
     32 import android.content.res.Resources;
     33 import android.view.InflateException;
     34 import com.android.settingslib.drawer.Tile;
     35 import com.android.settingslib.drawer.TileUtils;
     36 import org.xmlpull.v1.XmlPullParser;
     37 import org.xmlpull.v1.XmlPullParserException;
     38 
     39 import java.io.IOException;
     40 import java.util.ArrayList;
     41 import java.util.List;
     42 
     43 public class SuggestionParser {
     44 
     45     private static final String TAG = "SuggestionParser";
     46 
     47     // If defined, only returns this suggestion if the feature is supported.
     48     public static final String META_DATA_REQUIRE_FEATURE = "com.android.settings.require_feature";
     49 
     50     // If defined, only display this optional step if an account of that type exists.
     51     private static final String META_DATA_REQUIRE_ACCOUNT = "com.android.settings.require_account";
     52 
     53     // If defined and not true, do not should optional step.
     54     private static final String META_DATA_IS_SUPPORTED = "com.android.settings.is_supported";
     55 
     56     /**
     57      * Allows suggestions to appear after a certain number of days, and to re-appear if dismissed.
     58      * For instance:
     59      * 0,10
     60      * Will appear immediately, but if the user removes it, it will come back after 10 days.
     61      *
     62      * Another example:
     63      * 10,30
     64      * Will only show up after 10 days, and then again after 30.
     65      */
     66     public static final String META_DATA_DISMISS_CONTROL = "com.android.settings.dismiss";
     67 
     68     // Shared prefs keys for storing dismissed state.
     69     // Index into current dismissed state.
     70     private static final String DISMISS_INDEX = "_dismiss_index";
     71     private static final String SETUP_TIME = "_setup_time";
     72     private static final String IS_DISMISSED = "_is_dismissed";
     73 
     74     private static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000;
     75 
     76     private final Context mContext;
     77     private final List<SuggestionCategory> mSuggestionList;
     78     private final ArrayMap<Pair<String, String>, Tile> addCache = new ArrayMap<>();
     79     private final SharedPreferences mSharedPrefs;
     80 
     81     public SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml) {
     82         mContext = context;
     83         mSuggestionList = (List<SuggestionCategory>) new SuggestionOrderInflater(mContext)
     84                 .parse(orderXml);
     85         mSharedPrefs = sharedPrefs;
     86     }
     87 
     88     public List<Tile> getSuggestions() {
     89         List<Tile> suggestions = new ArrayList<>();
     90         final int N = mSuggestionList.size();
     91         for (int i = 0; i < N; i++) {
     92             readSuggestions(mSuggestionList.get(i), suggestions);
     93         }
     94         return suggestions;
     95     }
     96 
     97     /**
     98      * Dismisses a suggestion, returns true if the suggestion has no more dismisses left and should
     99      * be disabled.
    100      */
    101     public boolean dismissSuggestion(Tile suggestion) {
    102         String keyBase = suggestion.intent.getComponent().flattenToShortString();
    103         int index = mSharedPrefs.getInt(keyBase + DISMISS_INDEX, 0);
    104         String dismissControl = suggestion.metaData.getString(META_DATA_DISMISS_CONTROL);
    105         if (dismissControl == null || parseDismissString(dismissControl).length == index) {
    106             return true;
    107         }
    108         mSharedPrefs.edit()
    109                 .putBoolean(keyBase + IS_DISMISSED, true)
    110                 .commit();
    111         return false;
    112     }
    113 
    114     private void readSuggestions(SuggestionCategory category, List<Tile> suggestions) {
    115         int countBefore = suggestions.size();
    116         Intent intent = new Intent(Intent.ACTION_MAIN);
    117         intent.addCategory(category.category);
    118         if (category.pkg != null) {
    119             intent.setPackage(category.pkg);
    120         }
    121         TileUtils.getTilesForIntent(mContext, new UserHandle(UserHandle.myUserId()), intent,
    122                 addCache, null, suggestions, true, false);
    123         for (int i = countBefore; i < suggestions.size(); i++) {
    124             if (!isAvailable(suggestions.get(i)) ||
    125                     !isSupported(suggestions.get(i)) ||
    126                     !satisfiesRequiredAccount(suggestions.get(i)) ||
    127                     isDismissed(suggestions.get(i))) {
    128                 suggestions.remove(i--);
    129             }
    130         }
    131         if (!category.multiple && suggestions.size() > (countBefore + 1)) {
    132             // If there are too many, remove them all and only re-add the one with the highest
    133             // priority.
    134             Tile item = suggestions.remove(suggestions.size() - 1);
    135             while (suggestions.size() > countBefore) {
    136                 Tile last = suggestions.remove(suggestions.size() - 1);
    137                 if (last.priority > item.priority) {
    138                     item = last;
    139                 }
    140             }
    141             // If category is marked as done, do not add any item.
    142             if (!isCategoryDone(category.category)) {
    143                 suggestions.add(item);
    144             }
    145         }
    146     }
    147 
    148     private boolean isAvailable(Tile suggestion) {
    149         String featureRequired = suggestion.metaData.getString(META_DATA_REQUIRE_FEATURE);
    150         if (featureRequired != null) {
    151             return mContext.getPackageManager().hasSystemFeature(featureRequired);
    152         }
    153         return true;
    154     }
    155 
    156     public boolean satisfiesRequiredAccount(Tile suggestion) {
    157         String requiredAccountType = suggestion.metaData.getString(META_DATA_REQUIRE_ACCOUNT);
    158         if (requiredAccountType == null) {
    159             return true;
    160         }
    161         AccountManager accountManager = AccountManager.get(mContext);
    162         Account[] accounts = accountManager.getAccountsByType(requiredAccountType);
    163         return accounts.length > 0;
    164     }
    165 
    166     public boolean isSupported(Tile suggestion) {
    167         int isSupportedResource = suggestion.metaData.getInt(META_DATA_IS_SUPPORTED);
    168         try {
    169             if (suggestion.intent == null) {
    170                 return false;
    171             }
    172             final Resources res = mContext.getPackageManager().getResourcesForActivity(
    173                     suggestion.intent.getComponent());
    174             return isSupportedResource != 0 ? res.getBoolean(isSupportedResource) : true;
    175         } catch (PackageManager.NameNotFoundException e) {
    176             Log.w(TAG, "Cannot find resources for " + suggestion.intent.getComponent());
    177             return false;
    178         } catch (Resources.NotFoundException e) {
    179             Log.w(TAG, "Cannot find resources for " + suggestion.intent.getComponent(), e);
    180             return false;
    181         }
    182     }
    183 
    184     public boolean isCategoryDone(String category) {
    185         String name = Settings.Secure.COMPLETED_CATEGORY_PREFIX + category;
    186         return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) != 0;
    187     }
    188 
    189     public void markCategoryDone(String category) {
    190         String name = Settings.Secure.COMPLETED_CATEGORY_PREFIX + category;
    191         Settings.Secure.putInt(mContext.getContentResolver(), name, 1);
    192     }
    193 
    194     private boolean isDismissed(Tile suggestion) {
    195         Object dismissObj = suggestion.metaData.get(META_DATA_DISMISS_CONTROL);
    196         if (dismissObj == null) {
    197             return false;
    198         }
    199         String dismissControl = String.valueOf(dismissObj);
    200         String keyBase = suggestion.intent.getComponent().flattenToShortString();
    201         if (!mSharedPrefs.contains(keyBase + SETUP_TIME)) {
    202             mSharedPrefs.edit()
    203                     .putLong(keyBase + SETUP_TIME, System.currentTimeMillis())
    204                     .commit();
    205         }
    206         // Default to dismissed, so that we can have suggestions that only first appear after
    207         // some number of days.
    208         if (!mSharedPrefs.getBoolean(keyBase + IS_DISMISSED, true)) {
    209             return false;
    210         }
    211         int index = mSharedPrefs.getInt(keyBase + DISMISS_INDEX, 0);
    212         int currentDismiss = parseDismissString(dismissControl)[index];
    213         long time = getEndTime(mSharedPrefs.getLong(keyBase + SETUP_TIME, 0), currentDismiss);
    214         if (System.currentTimeMillis() >= time) {
    215             // Dismiss timeout has passed, undismiss it.
    216             mSharedPrefs.edit()
    217                     .putBoolean(keyBase + IS_DISMISSED, false)
    218                     .putInt(keyBase + DISMISS_INDEX, index + 1)
    219                     .commit();
    220             return false;
    221         }
    222         return true;
    223     }
    224 
    225     private long getEndTime(long startTime, int daysDelay) {
    226         long days = daysDelay * MILLIS_IN_DAY;
    227         return startTime + days;
    228     }
    229 
    230     private int[] parseDismissString(String dismissControl) {
    231         String[] dismissStrs = dismissControl.split(",");
    232         int[] dismisses = new int[dismissStrs.length];
    233         for (int i = 0; i < dismissStrs.length; i++) {
    234             dismisses[i] = Integer.parseInt(dismissStrs[i]);
    235         }
    236         return dismisses;
    237     }
    238 
    239     private static class SuggestionCategory {
    240         public String category;
    241         public String pkg;
    242         public boolean multiple;
    243     }
    244 
    245     private static class SuggestionOrderInflater {
    246         private static final String TAG_LIST = "optional-steps";
    247         private static final String TAG_ITEM = "step";
    248 
    249         private static final String ATTR_CATEGORY = "category";
    250         private static final String ATTR_PACKAGE = "package";
    251         private static final String ATTR_MULTIPLE = "multiple";
    252 
    253         private final Context mContext;
    254 
    255         public SuggestionOrderInflater(Context context) {
    256             mContext = context;
    257         }
    258 
    259         public Object parse(int resource) {
    260             XmlPullParser parser = mContext.getResources().getXml(resource);
    261             final AttributeSet attrs = Xml.asAttributeSet(parser);
    262             try {
    263                 // Look for the root node.
    264                 int type;
    265                 do {
    266                     type = parser.next();
    267                 } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
    268 
    269                 if (type != XmlPullParser.START_TAG) {
    270                     throw new InflateException(parser.getPositionDescription()
    271                             + ": No start tag found!");
    272                 }
    273 
    274                 // Temp is the root that was found in the xml
    275                 Object xmlRoot = onCreateItem(parser.getName(), attrs);
    276 
    277                 // Inflate all children under temp
    278                 rParse(parser, xmlRoot, attrs);
    279                 return xmlRoot;
    280             } catch (XmlPullParserException | IOException e) {
    281                 Log.w(TAG, "Problem parser resource " + resource, e);
    282                 return null;
    283             }
    284         }
    285 
    286         /**
    287          * Recursive method used to descend down the xml hierarchy and instantiate
    288          * items, instantiate their children.
    289          */
    290         private void rParse(XmlPullParser parser, Object parent, final AttributeSet attrs)
    291                 throws XmlPullParserException, IOException {
    292             final int depth = parser.getDepth();
    293 
    294             int type;
    295             while (((type = parser.next()) != XmlPullParser.END_TAG ||
    296                     parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
    297                 if (type != XmlPullParser.START_TAG) {
    298                     continue;
    299                 }
    300 
    301                 final String name = parser.getName();
    302 
    303                 Object item = onCreateItem(name, attrs);
    304                 onAddChildItem(parent, item);
    305                 rParse(parser, item, attrs);
    306             }
    307         }
    308 
    309         protected void onAddChildItem(Object parent, Object child) {
    310             if (parent instanceof List<?> && child instanceof SuggestionCategory) {
    311                 ((List<SuggestionCategory>) parent).add((SuggestionCategory) child);
    312             } else {
    313                 throw new IllegalArgumentException("Parent was not a list");
    314             }
    315         }
    316 
    317         protected Object onCreateItem(String name, AttributeSet attrs) {
    318             if (name.equals(TAG_LIST)) {
    319                 return new ArrayList<SuggestionCategory>();
    320             } else if (name.equals(TAG_ITEM)) {
    321                 SuggestionCategory category = new SuggestionCategory();
    322                 category.category = attrs.getAttributeValue(null, ATTR_CATEGORY);
    323                 category.pkg = attrs.getAttributeValue(null, ATTR_PACKAGE);
    324                 String multiple = attrs.getAttributeValue(null, ATTR_MULTIPLE);
    325                 category.multiple = !TextUtils.isEmpty(multiple) && Boolean.parseBoolean(multiple);
    326                 return category;
    327             } else {
    328                 throw new IllegalArgumentException("Unknown item " + name);
    329             }
    330         }
    331     }
    332 }
    333 
    334