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.server.pm; 17 18 import android.annotation.Nullable; 19 import android.annotation.UserIdInt; 20 import android.content.ComponentName; 21 import android.content.Intent; 22 import android.content.pm.ActivityInfo; 23 import android.content.pm.ResolveInfo; 24 import android.content.pm.ShortcutInfo; 25 import android.content.res.TypedArray; 26 import android.content.res.XmlResourceParser; 27 import android.text.TextUtils; 28 import android.util.ArraySet; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.util.Slog; 32 import android.util.TypedValue; 33 import android.util.Xml; 34 35 import com.android.internal.R; 36 import com.android.internal.annotations.VisibleForTesting; 37 38 import org.xmlpull.v1.XmlPullParser; 39 import org.xmlpull.v1.XmlPullParserException; 40 41 import java.io.IOException; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Set; 45 46 public class ShortcutParser { 47 private static final String TAG = ShortcutService.TAG; 48 49 private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE 50 51 @VisibleForTesting 52 static final String METADATA_KEY = "android.app.shortcuts"; 53 54 private static final String TAG_SHORTCUTS = "shortcuts"; 55 private static final String TAG_SHORTCUT = "shortcut"; 56 private static final String TAG_INTENT = "intent"; 57 private static final String TAG_CATEGORIES = "categories"; 58 59 @Nullable parseShortcuts(ShortcutService service, String packageName, @UserIdInt int userId)60 public static List<ShortcutInfo> parseShortcuts(ShortcutService service, 61 String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException { 62 if (ShortcutService.DEBUG) { 63 Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d", 64 packageName, userId)); 65 } 66 final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId); 67 if (activities == null || activities.size() == 0) { 68 return null; 69 } 70 71 List<ShortcutInfo> result = null; 72 73 try { 74 final int size = activities.size(); 75 for (int i = 0; i < size; i++) { 76 final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo; 77 if (activityInfoNoMetadata == null) { 78 continue; 79 } 80 81 final ActivityInfo activityInfoWithMetadata = 82 service.getActivityInfoWithMetadata( 83 activityInfoNoMetadata.getComponentName(), userId); 84 if (activityInfoWithMetadata != null) { 85 result = parseShortcutsOneFile( 86 service, activityInfoWithMetadata, packageName, userId, result); 87 } 88 } 89 } catch (RuntimeException e) { 90 // Resource ID mismatch may cause various runtime exceptions when parsing XMLs, 91 // But we don't crash the device, so just swallow them. 92 service.wtf( 93 "Exception caught while parsing shortcut XML for package=" + packageName, e); 94 return null; 95 } 96 return result; 97 } 98 parseShortcutsOneFile( ShortcutService service, ActivityInfo activityInfo, String packageName, @UserIdInt int userId, List<ShortcutInfo> result)99 private static List<ShortcutInfo> parseShortcutsOneFile( 100 ShortcutService service, 101 ActivityInfo activityInfo, String packageName, @UserIdInt int userId, 102 List<ShortcutInfo> result) throws IOException, XmlPullParserException { 103 if (ShortcutService.DEBUG) { 104 Slog.d(TAG, String.format( 105 "Checking main activity %s", activityInfo.getComponentName())); 106 } 107 108 XmlResourceParser parser = null; 109 try { 110 parser = service.injectXmlMetaData(activityInfo, METADATA_KEY); 111 if (parser == null) { 112 return result; 113 } 114 115 final ComponentName activity = new ComponentName(packageName, activityInfo.name); 116 117 final AttributeSet attrs = Xml.asAttributeSet(parser); 118 119 int type; 120 121 int rank = 0; 122 final int maxShortcuts = service.getMaxActivityShortcuts(); 123 int numShortcuts = 0; 124 125 // We instantiate ShortcutInfo at <shortcut>, but we add it to the list at </shortcut>, 126 // after parsing <intent>. We keep the current one in here. 127 ShortcutInfo currentShortcut = null; 128 129 Set<String> categories = null; 130 final ArrayList<Intent> intents = new ArrayList<>(); 131 132 outer: 133 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 134 && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) { 135 final int depth = parser.getDepth(); 136 final String tag = parser.getName(); 137 138 // When a shortcut tag is closing, publish. 139 if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) { 140 if (currentShortcut == null) { 141 // Shortcut was invalid. 142 continue; 143 } 144 final ShortcutInfo si = currentShortcut; 145 currentShortcut = null; // Make sure to null out for the next iteration. 146 147 if (si.isEnabled()) { 148 if (intents.size() == 0) { 149 Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it."); 150 continue; 151 } 152 } else { 153 // Just set the default intent to disabled shortcuts. 154 intents.clear(); 155 intents.add(new Intent(Intent.ACTION_VIEW)); 156 } 157 158 if (numShortcuts >= maxShortcuts) { 159 Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for " 160 + activityInfo.getComponentName() + ". Skipping the rest."); 161 return result; 162 } 163 164 // Same flag as what TaskStackBuilder adds. 165 intents.get(0).addFlags( 166 Intent.FLAG_ACTIVITY_NEW_TASK | 167 Intent.FLAG_ACTIVITY_CLEAR_TASK | 168 Intent.FLAG_ACTIVITY_TASK_ON_HOME); 169 try { 170 si.setIntents(intents.toArray(new Intent[intents.size()])); 171 } catch (RuntimeException e) { 172 // This shouldn't happen because intents in XML can't have complicated 173 // extras, but just in case Intent.parseIntent() supports such a thing one 174 // day. 175 Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it."); 176 continue; 177 } 178 intents.clear(); 179 180 if (categories != null) { 181 si.setCategories(categories); 182 categories = null; 183 } 184 185 if (result == null) { 186 result = new ArrayList<>(); 187 } 188 result.add(si); 189 numShortcuts++; 190 rank++; 191 if (ShortcutService.DEBUG) { 192 Slog.d(TAG, "Shortcut added: " + si.toInsecureString()); 193 } 194 continue; 195 } 196 197 // Otherwise, just look at start tags. 198 if (type != XmlPullParser.START_TAG) { 199 continue; 200 } 201 202 if (depth == 1 && TAG_SHORTCUTS.equals(tag)) { 203 continue; // Root tag. 204 } 205 if (depth == 2 && TAG_SHORTCUT.equals(tag)) { 206 final ShortcutInfo si = parseShortcutAttributes( 207 service, attrs, packageName, activity, userId, rank); 208 if (si == null) { 209 // Shortcut was invalid. 210 continue; 211 } 212 if (ShortcutService.DEBUG) { 213 Slog.d(TAG, "Shortcut found: " + si.toInsecureString()); 214 } 215 if (result != null) { 216 for (int i = result.size() - 1; i >= 0; i--) { 217 if (si.getId().equals(result.get(i).getId())) { 218 Log.e(TAG, "Duplicate shortcut ID detected. Skipping it."); 219 continue outer; 220 } 221 } 222 } 223 currentShortcut = si; 224 categories = null; 225 continue; 226 } 227 if (depth == 3 && TAG_INTENT.equals(tag)) { 228 if ((currentShortcut == null) 229 || !currentShortcut.isEnabled()) { 230 Log.e(TAG, "Ignoring excessive intent tag."); 231 continue; 232 } 233 234 final Intent intent = Intent.parseIntent(service.mContext.getResources(), 235 parser, attrs); 236 if (TextUtils.isEmpty(intent.getAction())) { 237 Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity); 238 currentShortcut = null; // Invalidate the current shortcut. 239 continue; 240 } 241 intents.add(intent); 242 continue; 243 } 244 if (depth == 3 && TAG_CATEGORIES.equals(tag)) { 245 if ((currentShortcut == null) 246 || (currentShortcut.getCategories() != null)) { 247 continue; 248 } 249 final String name = parseCategories(service, attrs); 250 if (TextUtils.isEmpty(name)) { 251 Log.e(TAG, "Empty category found. activity=" + activity); 252 continue; 253 } 254 255 if (categories == null) { 256 categories = new ArraySet<>(); 257 } 258 categories.add(name); 259 continue; 260 } 261 262 Log.w(TAG, String.format("Invalid tag '%s' found at depth %d", tag, depth)); 263 } 264 } finally { 265 if (parser != null) { 266 parser.close(); 267 } 268 } 269 return result; 270 } 271 parseCategories(ShortcutService service, AttributeSet attrs)272 private static String parseCategories(ShortcutService service, AttributeSet attrs) { 273 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 274 R.styleable.ShortcutCategories); 275 try { 276 if (sa.getType(R.styleable.ShortcutCategories_name) == TypedValue.TYPE_STRING) { 277 return sa.getNonResourceString(R.styleable.ShortcutCategories_name); 278 } else { 279 Log.w(TAG, "android:name for shortcut category must be string literal."); 280 return null; 281 } 282 } finally { 283 sa.recycle(); 284 } 285 } 286 parseShortcutAttributes(ShortcutService service, AttributeSet attrs, String packageName, ComponentName activity, @UserIdInt int userId, int rank)287 private static ShortcutInfo parseShortcutAttributes(ShortcutService service, 288 AttributeSet attrs, String packageName, ComponentName activity, 289 @UserIdInt int userId, int rank) { 290 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 291 R.styleable.Shortcut); 292 try { 293 if (sa.getType(R.styleable.Shortcut_shortcutId) != TypedValue.TYPE_STRING) { 294 Log.w(TAG, "android:shortcutId must be string literal. activity=" + activity); 295 return null; 296 } 297 final String id = sa.getNonResourceString(R.styleable.Shortcut_shortcutId); 298 final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true); 299 final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0); 300 final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0); 301 final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0); 302 final int disabledMessageResId = sa.getResourceId( 303 R.styleable.Shortcut_shortcutDisabledMessage, 0); 304 305 if (TextUtils.isEmpty(id)) { 306 Log.w(TAG, "android:shortcutId must be provided. activity=" + activity); 307 return null; 308 } 309 if (titleResId == 0) { 310 Log.w(TAG, "android:shortcutShortLabel must be provided. activity=" + activity); 311 return null; 312 } 313 314 return createShortcutFromManifest( 315 service, 316 userId, 317 id, 318 packageName, 319 activity, 320 titleResId, 321 textResId, 322 disabledMessageResId, 323 rank, 324 iconResId, 325 enabled); 326 } finally { 327 sa.recycle(); 328 } 329 } 330 createShortcutFromManifest(ShortcutService service, @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, int titleResId, int textResId, int disabledMessageResId, int rank, int iconResId, boolean enabled)331 private static ShortcutInfo createShortcutFromManifest(ShortcutService service, 332 @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, 333 int titleResId, int textResId, int disabledMessageResId, 334 int rank, int iconResId, boolean enabled) { 335 336 final int flags = 337 (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) 338 | ShortcutInfo.FLAG_IMMUTABLE 339 | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0); 340 final int disabledReason = 341 enabled ? ShortcutInfo.DISABLED_REASON_NOT_DISABLED 342 : ShortcutInfo.DISABLED_REASON_BY_APP; 343 344 // Note we don't need to set resource names here yet. They'll be set when they're about 345 // to be published. 346 return new ShortcutInfo( 347 userId, 348 id, 349 packageName, 350 activityComponent, 351 null, // icon 352 null, // title string 353 titleResId, 354 null, // title res name 355 null, // text string 356 textResId, 357 null, // text res name 358 null, // disabled message string 359 disabledMessageResId, 360 null, // disabled message res name 361 null, // categories 362 null, // intent 363 rank, 364 null, // extras 365 service.injectCurrentTimeMillis(), 366 flags, 367 iconResId, 368 null, // icon res name 369 null, // bitmap path 370 disabledReason); 371 } 372 } 373