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