• 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.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 
SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml)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 
getSuggestions()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      */
dismissSuggestion(Tile suggestion)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 
readSuggestions(SuggestionCategory category, List<Tile> suggestions)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 
isAvailable(Tile suggestion)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 
satisfiesRequiredAccount(Tile suggestion)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 
isSupported(Tile suggestion)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 
isCategoryDone(String category)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 
markCategoryDone(String category)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 
isDismissed(Tile suggestion)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 
getEndTime(long startTime, int daysDelay)225     private long getEndTime(long startTime, int daysDelay) {
226         long days = daysDelay * MILLIS_IN_DAY;
227         return startTime + days;
228     }
229 
parseDismissString(String dismissControl)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 
SuggestionOrderInflater(Context context)255         public SuggestionOrderInflater(Context context) {
256             mContext = context;
257         }
258 
parse(int resource)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          */
rParse(XmlPullParser parser, Object parent, final AttributeSet attrs)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 
onAddChildItem(Object parent, Object child)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 
onCreateItem(String name, AttributeSet attrs)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