blob: 56b84415e9079a58cdcd209ac2c78384bf1b06de [file] [log] [blame]
Jason Monkf509d7e2016-01-07 16:22:53 -05001/*
Fan Zhange138ef12017-05-11 15:29:56 -07002 * Copyright (C) 2017 The Android Open Source Project
Jason Monkf509d7e2016-01-07 16:22:53 -05003 *
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
Fan Zhange138ef12017-05-11 15:29:56 -070014 * limitations under the License.
Jason Monkf509d7e2016-01-07 16:22:53 -050015 */
Fan Zhange138ef12017-05-11 15:29:56 -070016package com.android.settingslib.suggestions;
Jason Monkf509d7e2016-01-07 16:22:53 -050017
Ido Ofira560cd92017-01-24 13:27:36 -080018import android.Manifest;
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.annotation.RequiresPermission;
Jason Monkf509d7e2016-01-07 16:22:53 -050022import android.content.Context;
23import android.content.Intent;
Jason Monk294efa52016-01-26 13:57:37 -050024import android.content.SharedPreferences;
Ido Ofira560cd92017-01-24 13:27:36 -080025import android.content.pm.PackageManager;
26import android.content.pm.UserInfo;
27import android.content.res.Resources;
28import android.net.ConnectivityManager;
29import android.net.NetworkInfo;
Jason Monkf509d7e2016-01-07 16:22:53 -050030import android.os.UserHandle;
Ido Ofira560cd92017-01-24 13:27:36 -080031import android.os.UserManager;
32import android.provider.Settings;
33import android.support.annotation.VisibleForTesting;
Jason Monkf509d7e2016-01-07 16:22:53 -050034import android.text.TextUtils;
Fan Zhanga0f8d232017-05-08 09:33:04 -070035import android.text.format.DateUtils;
Jason Monkf509d7e2016-01-07 16:22:53 -050036import android.util.ArrayMap;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.util.Pair;
40import android.util.Xml;
41import android.view.InflateException;
Fan Zhanga0f8d232017-05-08 09:33:04 -070042
Jason Monkf509d7e2016-01-07 16:22:53 -050043import com.android.settingslib.drawer.Tile;
44import com.android.settingslib.drawer.TileUtils;
Fan Zhanga0f8d232017-05-08 09:33:04 -070045
Jason Monkf509d7e2016-01-07 16:22:53 -050046import org.xmlpull.v1.XmlPullParser;
47import org.xmlpull.v1.XmlPullParserException;
48
49import java.io.IOException;
50import java.util.ArrayList;
51import java.util.List;
52
53public class SuggestionParser {
54
55 private static final String TAG = "SuggestionParser";
56
Jason Monk294efa52016-01-26 13:57:37 -050057 // If defined, only returns this suggestion if the feature is supported.
58 public static final String META_DATA_REQUIRE_FEATURE = "com.android.settings.require_feature";
59
Ido Ofird193c672016-03-01 13:27:54 -080060 // If defined, only display this optional step if an account of that type exists.
61 private static final String META_DATA_REQUIRE_ACCOUNT = "com.android.settings.require_account";
62
63 // If defined and not true, do not should optional step.
64 private static final String META_DATA_IS_SUPPORTED = "com.android.settings.is_supported";
65
Ido Ofira560cd92017-01-24 13:27:36 -080066 // If defined, only display this optional step if the current user is of that type.
67 private static final String META_DATA_REQUIRE_USER_TYPE =
68 "com.android.settings.require_user_type";
69
70 // If defined, only display this optional step if a connection is available.
71 private static final String META_DATA_IS_CONNECTION_REQUIRED =
72 "com.android.settings.require_connection";
73
74 // The valid values that setup wizard recognizes for differentiating user types.
75 private static final String META_DATA_PRIMARY_USER_TYPE_VALUE = "primary";
76 private static final String META_DATA_ADMIN_USER_TYPE_VALUE = "admin";
77 private static final String META_DATA_GUEST_USER_TYPE_VALUE = "guest";
78 private static final String META_DATA_RESTRICTED_USER_TYPE_VALUE = "restricted";
79
Jason Monk294efa52016-01-26 13:57:37 -050080 /**
81 * Allows suggestions to appear after a certain number of days, and to re-appear if dismissed.
82 * For instance:
83 * 0,10
84 * Will appear immediately, but if the user removes it, it will come back after 10 days.
85 *
86 * Another example:
87 * 10,30
88 * Will only show up after 10 days, and then again after 30.
89 */
90 public static final String META_DATA_DISMISS_CONTROL = "com.android.settings.dismiss";
91
92 // Shared prefs keys for storing dismissed state.
93 // Index into current dismissed state.
Fan Zhang17ba12a2017-06-28 14:23:54 -070094 public static final String SETUP_TIME = "_setup_time";
Jason Monk294efa52016-01-26 13:57:37 -050095 private static final String IS_DISMISSED = "_is_dismissed";
96
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -080097 // Default dismiss control for smart suggestions.
Fan Zhangc07ae9c2017-08-03 17:33:18 -070098 private static final String DEFAULT_SMART_DISMISS_CONTROL = "0";
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -080099
Jason Monkf509d7e2016-01-07 16:22:53 -0500100 private final Context mContext;
101 private final List<SuggestionCategory> mSuggestionList;
Ido Ofira560cd92017-01-24 13:27:36 -0800102 private final ArrayMap<Pair<String, String>, Tile> mAddCache = new ArrayMap<>();
Jason Monk294efa52016-01-26 13:57:37 -0500103 private final SharedPreferences mSharedPrefs;
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700104 private final String mDefaultDismissControl;
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800105
Fan Zhanga0f8d232017-05-08 09:33:04 -0700106 public SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml,
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700107 String defaultDismissControl) {
Maurice Lamf74b9e52017-03-23 14:58:47 -0700108 this(
109 context,
110 sharedPrefs,
111 (List<SuggestionCategory>) new SuggestionOrderInflater(context).parse(orderXml),
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700112 defaultDismissControl);
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800113 }
114
115 public SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml) {
Fan Zhanga0f8d232017-05-08 09:33:04 -0700116 this(context, sharedPrefs, orderXml, DEFAULT_SMART_DISMISS_CONTROL);
Jason Monkf509d7e2016-01-07 16:22:53 -0500117 }
118
Ido Ofira560cd92017-01-24 13:27:36 -0800119 @VisibleForTesting
Maurice Lamf74b9e52017-03-23 14:58:47 -0700120 public SuggestionParser(
121 Context context,
122 SharedPreferences sharedPrefs,
123 List<SuggestionCategory> suggestionList,
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700124 String defaultDismissControl) {
Ido Ofira560cd92017-01-24 13:27:36 -0800125 mContext = context;
Maurice Lamf74b9e52017-03-23 14:58:47 -0700126 mSuggestionList = suggestionList;
Ido Ofira560cd92017-01-24 13:27:36 -0800127 mSharedPrefs = sharedPrefs;
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700128 mDefaultDismissControl = defaultDismissControl;
Ido Ofira560cd92017-01-24 13:27:36 -0800129 }
130
Fan Zhange138ef12017-05-11 15:29:56 -0700131 public SuggestionList getSuggestions(boolean isSmartSuggestionEnabled) {
132 final SuggestionList suggestionList = new SuggestionList();
Jason Monkf509d7e2016-01-07 16:22:53 -0500133 final int N = mSuggestionList.size();
134 for (int i = 0; i < N; i++) {
Maurice Lamf74b9e52017-03-23 14:58:47 -0700135 final SuggestionCategory category = mSuggestionList.get(i);
Fan Zhanga0f8d232017-05-08 09:33:04 -0700136 if (category.exclusive && !isExclusiveCategoryExpired(category)) {
Maurice Lamf74b9e52017-03-23 14:58:47 -0700137 // If suggestions from an exclusive category are present, parsing is stopped
138 // and only suggestions from that category are displayed. Note that subsequent
139 // exclusive categories are also ignored.
Fan Zhanga0f8d232017-05-08 09:33:04 -0700140 final List<Tile> exclusiveSuggestions = new ArrayList<>();
141
142 // Read suggestion and force isSmartSuggestion to be false so the rule defined
143 // from each suggestion itself is used.
144 readSuggestions(category, exclusiveSuggestions, false /* isSmartSuggestion */);
Maurice Lamf74b9e52017-03-23 14:58:47 -0700145 if (!exclusiveSuggestions.isEmpty()) {
Fan Zhange138ef12017-05-11 15:29:56 -0700146 final SuggestionList exclusiveList = new SuggestionList();
147 exclusiveList.addSuggestions(category, exclusiveSuggestions);
148 return exclusiveList;
Maurice Lamf74b9e52017-03-23 14:58:47 -0700149 }
150 } else {
Fan Zhanga0f8d232017-05-08 09:33:04 -0700151 // Either the category is not exclusive, or the exclusiveness expired so we should
152 // treat it as a normal category.
Fan Zhange138ef12017-05-11 15:29:56 -0700153 final List<Tile> suggestions = new ArrayList<>();
Maurice Lamf74b9e52017-03-23 14:58:47 -0700154 readSuggestions(category, suggestions, isSmartSuggestionEnabled);
Fan Zhange138ef12017-05-11 15:29:56 -0700155 suggestionList.addSuggestions(category, suggestions);
Maurice Lamf74b9e52017-03-23 14:58:47 -0700156 }
Jason Monkf509d7e2016-01-07 16:22:53 -0500157 }
Fan Zhange138ef12017-05-11 15:29:56 -0700158 return suggestionList;
Jason Monkf509d7e2016-01-07 16:22:53 -0500159 }
160
Jason Monk294efa52016-01-26 13:57:37 -0500161 /**
162 * Dismisses a suggestion, returns true if the suggestion has no more dismisses left and should
163 * be disabled.
164 */
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700165 public boolean dismissSuggestion(Tile suggestion) {
166 final String keyBase = suggestion.intent.getComponent().flattenToShortString();
Jason Monk294efa52016-01-26 13:57:37 -0500167 mSharedPrefs.edit()
168 .putBoolean(keyBase + IS_DISMISSED, true)
169 .commit();
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700170 return true;
Jason Monk294efa52016-01-26 13:57:37 -0500171 }
172
Ido Ofira560cd92017-01-24 13:27:36 -0800173 @VisibleForTesting
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800174 public void filterSuggestions(
Fan Zhanga0f8d232017-05-08 09:33:04 -0700175 List<Tile> suggestions, int countBefore, boolean isSmartSuggestionEnabled) {
Ido Ofira560cd92017-01-24 13:27:36 -0800176 for (int i = countBefore; i < suggestions.size(); i++) {
177 if (!isAvailable(suggestions.get(i)) ||
178 !isSupported(suggestions.get(i)) ||
179 !satisifesRequiredUserType(suggestions.get(i)) ||
180 !satisfiesRequiredAccount(suggestions.get(i)) ||
181 !satisfiesConnectivity(suggestions.get(i)) ||
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800182 isDismissed(suggestions.get(i), isSmartSuggestionEnabled)) {
Ido Ofira560cd92017-01-24 13:27:36 -0800183 suggestions.remove(i--);
184 }
185 }
186 }
187
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800188 @VisibleForTesting
189 void readSuggestions(
Fan Zhanga0f8d232017-05-08 09:33:04 -0700190 SuggestionCategory category, List<Tile> suggestions, boolean isSmartSuggestionEnabled) {
Jason Monkf509d7e2016-01-07 16:22:53 -0500191 int countBefore = suggestions.size();
192 Intent intent = new Intent(Intent.ACTION_MAIN);
193 intent.addCategory(category.category);
194 if (category.pkg != null) {
195 intent.setPackage(category.pkg);
196 }
197 TileUtils.getTilesForIntent(mContext, new UserHandle(UserHandle.myUserId()), intent,
Ajay Nadathurd1b730b2017-09-14 17:12:27 -0700198 mAddCache, null, suggestions, true, false, false, true /* shouldUpdateTiles */);
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800199 filterSuggestions(suggestions, countBefore, isSmartSuggestionEnabled);
Jason Monkf509d7e2016-01-07 16:22:53 -0500200 if (!category.multiple && suggestions.size() > (countBefore + 1)) {
201 // If there are too many, remove them all and only re-add the one with the highest
202 // priority.
203 Tile item = suggestions.remove(suggestions.size() - 1);
204 while (suggestions.size() > countBefore) {
205 Tile last = suggestions.remove(suggestions.size() - 1);
206 if (last.priority > item.priority) {
207 item = last;
208 }
209 }
Ido Ofird193c672016-03-01 13:27:54 -0800210 // If category is marked as done, do not add any item.
211 if (!isCategoryDone(category.category)) {
212 suggestions.add(item);
213 }
Jason Monkf509d7e2016-01-07 16:22:53 -0500214 }
215 }
216
Jason Monk294efa52016-01-26 13:57:37 -0500217 private boolean isAvailable(Tile suggestion) {
Ido Ofira560cd92017-01-24 13:27:36 -0800218 final String featuresRequired = suggestion.metaData.getString(META_DATA_REQUIRE_FEATURE);
219 if (featuresRequired != null) {
220 for (String feature : featuresRequired.split(",")) {
221 if (TextUtils.isEmpty(feature)) {
222 Log.w(TAG, "Found empty substring when parsing required features: "
223 + featuresRequired);
224 } else if (!mContext.getPackageManager().hasSystemFeature(feature)) {
225 Log.i(TAG, suggestion.title + " requires unavailable feature " + feature);
226 return false;
227 }
228 }
229 }
230 return true;
231 }
232
233 @RequiresPermission(Manifest.permission.MANAGE_USERS)
234 private boolean satisifesRequiredUserType(Tile suggestion) {
235 final String requiredUser = suggestion.metaData.getString(META_DATA_REQUIRE_USER_TYPE);
236 if (requiredUser != null) {
237 final UserManager userManager = mContext.getSystemService(UserManager.class);
238 UserInfo userInfo = userManager.getUserInfo(UserHandle.myUserId());
239 for (String userType : requiredUser.split("\\|")) {
240 final boolean primaryUserCondtionMet = userInfo.isPrimary()
241 && META_DATA_PRIMARY_USER_TYPE_VALUE.equals(userType);
242 final boolean adminUserConditionMet = userInfo.isAdmin()
243 && META_DATA_ADMIN_USER_TYPE_VALUE.equals(userType);
244 final boolean guestUserCondtionMet = userInfo.isGuest()
245 && META_DATA_GUEST_USER_TYPE_VALUE.equals(userType);
246 final boolean restrictedUserCondtionMet = userInfo.isRestricted()
247 && META_DATA_RESTRICTED_USER_TYPE_VALUE.equals(userType);
248 if (primaryUserCondtionMet || adminUserConditionMet || guestUserCondtionMet
249 || restrictedUserCondtionMet) {
250 return true;
251 }
252 }
253 Log.i(TAG, suggestion.title + " requires user type " + requiredUser);
254 return false;
Jason Monk294efa52016-01-26 13:57:37 -0500255 }
256 return true;
257 }
258
Ido Ofird193c672016-03-01 13:27:54 -0800259 public boolean satisfiesRequiredAccount(Tile suggestion) {
Ido Ofira560cd92017-01-24 13:27:36 -0800260 final String requiredAccountType = suggestion.metaData.getString(META_DATA_REQUIRE_ACCOUNT);
Ido Ofird193c672016-03-01 13:27:54 -0800261 if (requiredAccountType == null) {
262 return true;
263 }
264 AccountManager accountManager = AccountManager.get(mContext);
265 Account[] accounts = accountManager.getAccountsByType(requiredAccountType);
Ido Ofira560cd92017-01-24 13:27:36 -0800266 boolean satisfiesRequiredAccount = accounts.length > 0;
267 if (!satisfiesRequiredAccount) {
268 Log.i(TAG, suggestion.title + " requires unavailable account type "
269 + requiredAccountType);
270 }
271 return satisfiesRequiredAccount;
Ido Ofird193c672016-03-01 13:27:54 -0800272 }
273
274 public boolean isSupported(Tile suggestion) {
Ido Ofira560cd92017-01-24 13:27:36 -0800275 final int isSupportedResource = suggestion.metaData.getInt(META_DATA_IS_SUPPORTED);
Ido Ofird193c672016-03-01 13:27:54 -0800276 try {
277 if (suggestion.intent == null) {
278 return false;
279 }
280 final Resources res = mContext.getPackageManager().getResourcesForActivity(
281 suggestion.intent.getComponent());
Ido Ofira560cd92017-01-24 13:27:36 -0800282 boolean isSupported =
283 isSupportedResource != 0 ? res.getBoolean(isSupportedResource) : true;
284 if (!isSupported) {
285 Log.i(TAG, suggestion.title + " requires unsupported resource "
286 + isSupportedResource);
287 }
288 return isSupported;
Ido Ofird193c672016-03-01 13:27:54 -0800289 } catch (PackageManager.NameNotFoundException e) {
290 Log.w(TAG, "Cannot find resources for " + suggestion.intent.getComponent());
291 return false;
Salvador Martinez60d92b32016-08-04 13:37:52 -0700292 } catch (Resources.NotFoundException e) {
293 Log.w(TAG, "Cannot find resources for " + suggestion.intent.getComponent(), e);
294 return false;
Ido Ofird193c672016-03-01 13:27:54 -0800295 }
296 }
297
Ido Ofira560cd92017-01-24 13:27:36 -0800298 private boolean satisfiesConnectivity(Tile suggestion) {
299 final boolean isConnectionRequired =
300 suggestion.metaData.getBoolean(META_DATA_IS_CONNECTION_REQUIRED);
301 if (!isConnectionRequired) {
Fan Zhanga0f8d232017-05-08 09:33:04 -0700302 return true;
Ido Ofira560cd92017-01-24 13:27:36 -0800303 }
304 ConnectivityManager cm =
305 (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
306 NetworkInfo netInfo = cm.getActiveNetworkInfo();
307 boolean satisfiesConnectivity = netInfo != null && netInfo.isConnectedOrConnecting();
308 if (!satisfiesConnectivity) {
309 Log.i(TAG, suggestion.title + " is missing required connection.");
310 }
311 return satisfiesConnectivity;
312 }
313
Ido Ofird193c672016-03-01 13:27:54 -0800314 public boolean isCategoryDone(String category) {
315 String name = Settings.Secure.COMPLETED_CATEGORY_PREFIX + category;
316 return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) != 0;
317 }
318
319 public void markCategoryDone(String category) {
320 String name = Settings.Secure.COMPLETED_CATEGORY_PREFIX + category;
321 Settings.Secure.putInt(mContext.getContentResolver(), name, 1);
322 }
323
Fan Zhanga0f8d232017-05-08 09:33:04 -0700324 /**
325 * Whether or not the category's exclusiveness has expired.
326 */
327 private boolean isExclusiveCategoryExpired(SuggestionCategory category) {
328 final String keySetupTime = category.category + SETUP_TIME;
329 final long currentTime = System.currentTimeMillis();
330 if (!mSharedPrefs.contains(keySetupTime)) {
331 mSharedPrefs.edit()
332 .putLong(keySetupTime, currentTime)
333 .commit();
334 }
335 if (category.exclusiveExpireDaysInMillis < 0) {
336 // negative means never expires
337 return false;
338 }
339 final long setupTime = mSharedPrefs.getLong(keySetupTime, 0);
Fan Zhang89cff1a2017-06-03 17:46:55 -0700340 final long elapsedTime = currentTime - setupTime;
341 Log.d(TAG, "Day " + elapsedTime / DateUtils.DAY_IN_MILLIS + " for " + category.category);
342 return elapsedTime > category.exclusiveExpireDaysInMillis;
Fan Zhanga0f8d232017-05-08 09:33:04 -0700343 }
344
Fan Zhang7b77cf82017-07-21 10:21:36 -0700345 @VisibleForTesting
346 boolean isDismissed(Tile suggestion, boolean isSmartSuggestionEnabled) {
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800347 String dismissControl = getDismissControl(suggestion, isSmartSuggestionEnabled);
Jason Monk294efa52016-01-26 13:57:37 -0500348 String keyBase = suggestion.intent.getComponent().flattenToShortString();
349 if (!mSharedPrefs.contains(keyBase + SETUP_TIME)) {
350 mSharedPrefs.edit()
351 .putLong(keyBase + SETUP_TIME, System.currentTimeMillis())
352 .commit();
353 }
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700354 // Check if it's already manually dismissed
355 final boolean isDismissed = mSharedPrefs.getBoolean(keyBase + IS_DISMISSED, false);
356 if (isDismissed) {
Fan Zhang7b77cf82017-07-21 10:21:36 -0700357 return true;
358 }
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700359 if (dismissControl == null) {
360 return false;
361 }
362 // Parse when suggestion should first appear. return true to artificially hide suggestion
363 // before then.
364 int firstAppearDay = parseDismissString(dismissControl);
365 long firstAppearDayInMs = getEndTime(mSharedPrefs.getLong(keyBase + SETUP_TIME, 0),
366 firstAppearDay);
367 if (System.currentTimeMillis() >= firstAppearDayInMs) {
Jason Monk294efa52016-01-26 13:57:37 -0500368 // Dismiss timeout has passed, undismiss it.
369 mSharedPrefs.edit()
370 .putBoolean(keyBase + IS_DISMISSED, false)
Jason Monk294efa52016-01-26 13:57:37 -0500371 .commit();
372 return false;
373 }
374 return true;
375 }
376
377 private long getEndTime(long startTime, int daysDelay) {
Fan Zhang17ba12a2017-06-28 14:23:54 -0700378 long days = daysDelay * DateUtils.DAY_IN_MILLIS;
Jason Monk294efa52016-01-26 13:57:37 -0500379 return startTime + days;
380 }
381
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700382 /**
383 * Parse the first int from a string formatted as "0,1,2..."
384 * The value means suggestion should first appear on Day X.
385 */
386 private int parseDismissString(String dismissControl) {
387 final String[] dismissStrs = dismissControl.split(",");
388 return Integer.parseInt(dismissStrs[0]);
Jason Monk294efa52016-01-26 13:57:37 -0500389 }
390
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800391 private String getDismissControl(Tile suggestion, boolean isSmartSuggestionEnabled) {
392 if (isSmartSuggestionEnabled) {
Fan Zhangc07ae9c2017-08-03 17:33:18 -0700393 return mDefaultDismissControl;
Soroosh Mariooryad56ce7662017-02-06 15:23:00 -0800394 } else {
395 return suggestion.metaData.getString(META_DATA_DISMISS_CONTROL);
396 }
397 }
398
Jason Monkf509d7e2016-01-07 16:22:53 -0500399 private static class SuggestionOrderInflater {
400 private static final String TAG_LIST = "optional-steps";
401 private static final String TAG_ITEM = "step";
402
403 private static final String ATTR_CATEGORY = "category";
404 private static final String ATTR_PACKAGE = "package";
405 private static final String ATTR_MULTIPLE = "multiple";
Maurice Lamf74b9e52017-03-23 14:58:47 -0700406 private static final String ATTR_EXCLUSIVE = "exclusive";
Fan Zhanga0f8d232017-05-08 09:33:04 -0700407 private static final String ATTR_EXCLUSIVE_EXPIRE_DAYS = "exclusiveExpireDays";
Jason Monkf509d7e2016-01-07 16:22:53 -0500408
409 private final Context mContext;
410
411 public SuggestionOrderInflater(Context context) {
412 mContext = context;
413 }
414
415 public Object parse(int resource) {
416 XmlPullParser parser = mContext.getResources().getXml(resource);
417 final AttributeSet attrs = Xml.asAttributeSet(parser);
418 try {
419 // Look for the root node.
420 int type;
421 do {
422 type = parser.next();
423 } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
424
425 if (type != XmlPullParser.START_TAG) {
426 throw new InflateException(parser.getPositionDescription()
427 + ": No start tag found!");
428 }
429
430 // Temp is the root that was found in the xml
431 Object xmlRoot = onCreateItem(parser.getName(), attrs);
432
433 // Inflate all children under temp
434 rParse(parser, xmlRoot, attrs);
435 return xmlRoot;
436 } catch (XmlPullParserException | IOException e) {
437 Log.w(TAG, "Problem parser resource " + resource, e);
438 return null;
439 }
440 }
441
442 /**
443 * Recursive method used to descend down the xml hierarchy and instantiate
444 * items, instantiate their children.
445 */
446 private void rParse(XmlPullParser parser, Object parent, final AttributeSet attrs)
447 throws XmlPullParserException, IOException {
448 final int depth = parser.getDepth();
449
450 int type;
451 while (((type = parser.next()) != XmlPullParser.END_TAG ||
452 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
453 if (type != XmlPullParser.START_TAG) {
454 continue;
455 }
456
457 final String name = parser.getName();
458
459 Object item = onCreateItem(name, attrs);
460 onAddChildItem(parent, item);
461 rParse(parser, item, attrs);
462 }
463 }
464
465 protected void onAddChildItem(Object parent, Object child) {
466 if (parent instanceof List<?> && child instanceof SuggestionCategory) {
467 ((List<SuggestionCategory>) parent).add((SuggestionCategory) child);
468 } else {
469 throw new IllegalArgumentException("Parent was not a list");
470 }
471 }
472
473 protected Object onCreateItem(String name, AttributeSet attrs) {
474 if (name.equals(TAG_LIST)) {
475 return new ArrayList<SuggestionCategory>();
476 } else if (name.equals(TAG_ITEM)) {
477 SuggestionCategory category = new SuggestionCategory();
478 category.category = attrs.getAttributeValue(null, ATTR_CATEGORY);
479 category.pkg = attrs.getAttributeValue(null, ATTR_PACKAGE);
480 String multiple = attrs.getAttributeValue(null, ATTR_MULTIPLE);
481 category.multiple = !TextUtils.isEmpty(multiple) && Boolean.parseBoolean(multiple);
Maurice Lamf74b9e52017-03-23 14:58:47 -0700482 String exclusive = attrs.getAttributeValue(null, ATTR_EXCLUSIVE);
483 category.exclusive =
484 !TextUtils.isEmpty(exclusive) && Boolean.parseBoolean(exclusive);
Fan Zhanga0f8d232017-05-08 09:33:04 -0700485 String expireDaysAttr = attrs.getAttributeValue(null,
486 ATTR_EXCLUSIVE_EXPIRE_DAYS);
487 long expireDays = !TextUtils.isEmpty(expireDaysAttr)
488 ? Integer.parseInt(expireDaysAttr)
489 : -1;
490 category.exclusiveExpireDaysInMillis = DateUtils.DAY_IN_MILLIS * expireDays;
Jason Monkf509d7e2016-01-07 16:22:53 -0500491 return category;
492 } else {
493 throw new IllegalArgumentException("Unknown item " + name);
494 }
495 }
496 }
497}
498