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.NonNull; 19 import android.annotation.Nullable; 20 import android.annotation.UserIdInt; 21 import android.content.ComponentName; 22 import android.content.Intent; 23 import android.content.pm.ActivityInfo; 24 import android.content.pm.ResolveInfo; 25 import android.content.pm.ShortcutInfo; 26 import android.content.res.TypedArray; 27 import android.content.res.XmlResourceParser; 28 import android.text.TextUtils; 29 import android.util.ArraySet; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.util.Slog; 33 import android.util.TypedValue; 34 import android.util.Xml; 35 36 import com.android.internal.R; 37 import com.android.internal.annotations.VisibleForTesting; 38 39 import org.xmlpull.v1.XmlPullParser; 40 import org.xmlpull.v1.XmlPullParserException; 41 42 import java.io.IOException; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Set; 46 47 public class ShortcutParser { 48 private static final String TAG = ShortcutService.TAG; 49 50 private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE 51 52 @VisibleForTesting 53 static final String METADATA_KEY = "android.app.shortcuts"; 54 55 private static final String TAG_SHORTCUTS = "shortcuts"; 56 private static final String TAG_SHORTCUT = "shortcut"; 57 private static final String TAG_INTENT = "intent"; 58 private static final String TAG_CATEGORIES = "categories"; 59 private static final String TAG_SHARE_TARGET = "share-target"; 60 private static final String TAG_DATA = "data"; 61 private static final String TAG_CATEGORY = "category"; 62 63 @Nullable parseShortcuts(ShortcutService service, String packageName, @UserIdInt int userId, @NonNull List<ShareTargetInfo> outShareTargets)64 public static List<ShortcutInfo> parseShortcuts(ShortcutService service, String packageName, 65 @UserIdInt int userId, @NonNull List<ShareTargetInfo> outShareTargets) 66 throws IOException, XmlPullParserException { 67 if (ShortcutService.DEBUG) { 68 Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d", 69 packageName, userId)); 70 } 71 final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId); 72 if (activities == null || activities.size() == 0) { 73 return null; 74 } 75 76 List<ShortcutInfo> result = null; 77 outShareTargets.clear(); 78 79 try { 80 final int size = activities.size(); 81 for (int i = 0; i < size; i++) { 82 final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo; 83 if (activityInfoNoMetadata == null) { 84 continue; 85 } 86 87 final ActivityInfo activityInfoWithMetadata = 88 service.getActivityInfoWithMetadata( 89 activityInfoNoMetadata.getComponentName(), userId); 90 if (activityInfoWithMetadata != null) { 91 result = parseShortcutsOneFile(service, activityInfoWithMetadata, packageName, 92 userId, result, outShareTargets); 93 } 94 } 95 } catch (RuntimeException e) { 96 // Resource ID mismatch may cause various runtime exceptions when parsing XMLs, 97 // But we don't crash the device, so just swallow them. 98 service.wtf( 99 "Exception caught while parsing shortcut XML for package=" + packageName, e); 100 return null; 101 } 102 return result; 103 } 104 parseShortcutsOneFile( ShortcutService service, ActivityInfo activityInfo, String packageName, @UserIdInt int userId, List<ShortcutInfo> result, @NonNull List<ShareTargetInfo> outShareTargets)105 private static List<ShortcutInfo> parseShortcutsOneFile( 106 ShortcutService service, 107 ActivityInfo activityInfo, String packageName, @UserIdInt int userId, 108 List<ShortcutInfo> result, @NonNull List<ShareTargetInfo> outShareTargets) 109 throws IOException, XmlPullParserException { 110 if (ShortcutService.DEBUG) { 111 Slog.d(TAG, String.format( 112 "Checking main activity %s", activityInfo.getComponentName())); 113 } 114 115 XmlResourceParser parser = null; 116 try { 117 parser = service.injectXmlMetaData(activityInfo, METADATA_KEY); 118 if (parser == null) { 119 return result; 120 } 121 122 final ComponentName activity = new ComponentName(packageName, activityInfo.name); 123 124 final AttributeSet attrs = Xml.asAttributeSet(parser); 125 126 int type; 127 128 int rank = 0; 129 final int maxShortcuts = service.getMaxActivityShortcuts(); 130 int numShortcuts = 0; 131 132 // We instantiate ShortcutInfo at <shortcut>, but we add it to the list at </shortcut>, 133 // after parsing <intent>. We keep the current one in here. 134 ShortcutInfo currentShortcut = null; 135 136 // We instantiate ShareTargetInfo at <share-target>, but add it to outShareTargets at 137 // </share-target>, after parsing <data> and <category>. We keep the current one here. 138 ShareTargetInfo currentShareTarget = null; 139 140 // Keeps parsed categories for both ShortcutInfo and ShareTargetInfo 141 Set<String> categories = null; 142 143 // Keeps parsed intents for ShortcutInfo 144 final ArrayList<Intent> intents = new ArrayList<>(); 145 146 // Keeps parsed data fields for ShareTargetInfo 147 final ArrayList<ShareTargetInfo.TargetData> dataList = new ArrayList<>(); 148 149 outer: 150 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 151 && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) { 152 final int depth = parser.getDepth(); 153 final String tag = parser.getName(); 154 155 // When a shortcut tag is closing, publish. 156 if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) { 157 if (currentShortcut == null) { 158 // Shortcut was invalid. 159 continue; 160 } 161 final ShortcutInfo si = currentShortcut; 162 currentShortcut = null; // Make sure to null out for the next iteration. 163 164 if (si.isEnabled()) { 165 if (intents.size() == 0) { 166 Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it."); 167 continue; 168 } 169 } else { 170 // Just set the default intent to disabled shortcuts. 171 intents.clear(); 172 intents.add(new Intent(Intent.ACTION_VIEW)); 173 } 174 175 if (numShortcuts >= maxShortcuts) { 176 Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for " 177 + activityInfo.getComponentName() + ". Skipping the rest."); 178 return result; 179 } 180 181 // Same flag as what TaskStackBuilder adds. 182 intents.get(0).addFlags( 183 Intent.FLAG_ACTIVITY_NEW_TASK | 184 Intent.FLAG_ACTIVITY_CLEAR_TASK | 185 Intent.FLAG_ACTIVITY_TASK_ON_HOME); 186 try { 187 si.setIntents(intents.toArray(new Intent[intents.size()])); 188 } catch (RuntimeException e) { 189 // This shouldn't happen because intents in XML can't have complicated 190 // extras, but just in case Intent.parseIntent() supports such a thing one 191 // day. 192 Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it."); 193 continue; 194 } 195 intents.clear(); 196 197 if (categories != null) { 198 si.setCategories(categories); 199 categories = null; 200 } 201 202 if (result == null) { 203 result = new ArrayList<>(); 204 } 205 result.add(si); 206 numShortcuts++; 207 rank++; 208 if (ShortcutService.DEBUG) { 209 Slog.d(TAG, "Shortcut added: " + si.toInsecureString()); 210 } 211 continue; 212 } 213 214 // When a share-target tag is closing, publish. 215 if ((type == XmlPullParser.END_TAG) && (depth == 2) 216 && (TAG_SHARE_TARGET.equals(tag))) { 217 if (currentShareTarget == null) { 218 // ShareTarget was invalid. 219 continue; 220 } 221 final ShareTargetInfo sti = currentShareTarget; 222 currentShareTarget = null; // Make sure to null out for the next iteration. 223 224 if (categories == null || categories.isEmpty() || dataList.isEmpty()) { 225 // Incomplete ShareTargetInfo. 226 continue; 227 } 228 229 final ShareTargetInfo newShareTarget = new ShareTargetInfo( 230 dataList.toArray(new ShareTargetInfo.TargetData[dataList.size()]), 231 sti.mTargetClass, categories.toArray(new String[categories.size()])); 232 outShareTargets.add(newShareTarget); 233 if (ShortcutService.DEBUG) { 234 Slog.d(TAG, "ShareTarget added: " + newShareTarget.toString()); 235 } 236 categories = null; 237 dataList.clear(); 238 } 239 240 // Otherwise, just look at start tags. 241 if (type != XmlPullParser.START_TAG) { 242 continue; 243 } 244 245 if (depth == 1 && TAG_SHORTCUTS.equals(tag)) { 246 continue; // Root tag. 247 } 248 if (depth == 2 && TAG_SHORTCUT.equals(tag)) { 249 final ShortcutInfo si = parseShortcutAttributes( 250 service, attrs, packageName, activity, userId, rank); 251 if (si == null) { 252 // Shortcut was invalid. 253 continue; 254 } 255 if (ShortcutService.DEBUG) { 256 Slog.d(TAG, "Shortcut found: " + si.toInsecureString()); 257 } 258 if (result != null) { 259 for (int i = result.size() - 1; i >= 0; i--) { 260 if (si.getId().equals(result.get(i).getId())) { 261 Log.e(TAG, "Duplicate shortcut ID detected. Skipping it."); 262 continue outer; 263 } 264 } 265 } 266 currentShortcut = si; 267 categories = null; 268 continue; 269 } 270 if (depth == 2 && TAG_SHARE_TARGET.equals(tag)) { 271 final ShareTargetInfo sti = parseShareTargetAttributes(service, attrs); 272 if (sti == null) { 273 // ShareTarget was invalid. 274 continue; 275 } 276 currentShareTarget = sti; 277 categories = null; 278 dataList.clear(); 279 continue; 280 } 281 if (depth == 3 && TAG_INTENT.equals(tag)) { 282 if ((currentShortcut == null) 283 || !currentShortcut.isEnabled()) { 284 Log.e(TAG, "Ignoring excessive intent tag."); 285 continue; 286 } 287 288 final Intent intent = Intent.parseIntent(service.mContext.getResources(), 289 parser, attrs); 290 if (TextUtils.isEmpty(intent.getAction())) { 291 Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity); 292 currentShortcut = null; // Invalidate the current shortcut. 293 continue; 294 } 295 intents.add(intent); 296 continue; 297 } 298 if (depth == 3 && TAG_CATEGORIES.equals(tag)) { 299 if ((currentShortcut == null) 300 || (currentShortcut.getCategories() != null)) { 301 continue; 302 } 303 final String name = parseCategories(service, attrs); 304 if (TextUtils.isEmpty(name)) { 305 Log.e(TAG, "Empty category found. activity=" + activity); 306 continue; 307 } 308 309 if (categories == null) { 310 categories = new ArraySet<>(); 311 } 312 categories.add(name); 313 continue; 314 } 315 if (depth == 3 && TAG_CATEGORY.equals(tag)) { 316 if ((currentShareTarget == null)) { 317 continue; 318 } 319 final String name = parseCategory(service, attrs); 320 if (TextUtils.isEmpty(name)) { 321 Log.e(TAG, "Empty category found. activity=" + activity); 322 continue; 323 } 324 325 if (categories == null) { 326 categories = new ArraySet<>(); 327 } 328 categories.add(name); 329 continue; 330 } 331 if (depth == 3 && TAG_DATA.equals(tag)) { 332 if ((currentShareTarget == null)) { 333 continue; 334 } 335 final ShareTargetInfo.TargetData data = parseShareTargetData(service, attrs); 336 if (data == null) { 337 Log.e(TAG, "Invalid data tag found. activity=" + activity); 338 continue; 339 } 340 dataList.add(data); 341 continue; 342 } 343 344 Log.w(TAG, String.format("Invalid tag '%s' found at depth %d", tag, depth)); 345 } 346 } finally { 347 if (parser != null) { 348 parser.close(); 349 } 350 } 351 return result; 352 } 353 parseCategories(ShortcutService service, AttributeSet attrs)354 private static String parseCategories(ShortcutService service, AttributeSet attrs) { 355 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 356 R.styleable.ShortcutCategories); 357 try { 358 if (sa.getType(R.styleable.ShortcutCategories_name) == TypedValue.TYPE_STRING) { 359 return sa.getNonResourceString(R.styleable.ShortcutCategories_name); 360 } else { 361 Log.w(TAG, "android:name for shortcut category must be string literal."); 362 return null; 363 } 364 } finally { 365 sa.recycle(); 366 } 367 } 368 parseShortcutAttributes(ShortcutService service, AttributeSet attrs, String packageName, ComponentName activity, @UserIdInt int userId, int rank)369 private static ShortcutInfo parseShortcutAttributes(ShortcutService service, 370 AttributeSet attrs, String packageName, ComponentName activity, 371 @UserIdInt int userId, int rank) { 372 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 373 R.styleable.Shortcut); 374 try { 375 if (sa.getType(R.styleable.Shortcut_shortcutId) != TypedValue.TYPE_STRING) { 376 Log.w(TAG, "android:shortcutId must be string literal. activity=" + activity); 377 return null; 378 } 379 final String id = sa.getNonResourceString(R.styleable.Shortcut_shortcutId); 380 final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true); 381 final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0); 382 final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0); 383 final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0); 384 final int disabledMessageResId = sa.getResourceId( 385 R.styleable.Shortcut_shortcutDisabledMessage, 0); 386 final int splashScreenThemeResId = sa.getResourceId( 387 R.styleable.Shortcut_splashScreenTheme, 0); 388 final String splashScreenThemeResName = splashScreenThemeResId != 0 389 ? service.mContext.getResources().getResourceName(splashScreenThemeResId) 390 : null; 391 392 if (TextUtils.isEmpty(id)) { 393 Log.w(TAG, "android:shortcutId must be provided. activity=" + activity); 394 return null; 395 } 396 if (titleResId == 0) { 397 Log.w(TAG, "android:shortcutShortLabel must be provided. activity=" + activity); 398 return null; 399 } 400 401 return createShortcutFromManifest( 402 service, 403 userId, 404 id, 405 packageName, 406 activity, 407 titleResId, 408 textResId, 409 disabledMessageResId, 410 rank, 411 iconResId, 412 enabled, 413 splashScreenThemeResName); 414 } finally { 415 sa.recycle(); 416 } 417 } 418 createShortcutFromManifest(ShortcutService service, @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, int titleResId, int textResId, int disabledMessageResId, int rank, int iconResId, boolean enabled, @Nullable String splashScreenThemeResName)419 private static ShortcutInfo createShortcutFromManifest(ShortcutService service, 420 @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, 421 int titleResId, int textResId, int disabledMessageResId, 422 int rank, int iconResId, boolean enabled, @Nullable String splashScreenThemeResName) { 423 424 final int flags = 425 (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) 426 | ShortcutInfo.FLAG_IMMUTABLE 427 | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0); 428 final int disabledReason = 429 enabled ? ShortcutInfo.DISABLED_REASON_NOT_DISABLED 430 : ShortcutInfo.DISABLED_REASON_BY_APP; 431 432 // Note we don't need to set resource names here yet. They'll be set when they're about 433 // to be published. 434 return new ShortcutInfo( 435 userId, 436 id, 437 packageName, 438 activityComponent, 439 null, // icon 440 null, // title string 441 titleResId, 442 null, // title res name 443 null, // text string 444 textResId, 445 null, // text res name 446 null, // disabled message string 447 disabledMessageResId, 448 null, // disabled message res name 449 null, // categories 450 null, // intent 451 rank, 452 null, // extras 453 service.injectCurrentTimeMillis(), 454 flags, 455 iconResId, 456 null, // icon res name 457 null, // bitmap path 458 null, // icon Url 459 disabledReason, 460 null /* persons */, 461 null /* locusId */, 462 splashScreenThemeResName); 463 } 464 parseCategory(ShortcutService service, AttributeSet attrs)465 private static String parseCategory(ShortcutService service, AttributeSet attrs) { 466 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 467 R.styleable.IntentCategory); 468 try { 469 if (sa.getType(R.styleable.IntentCategory_name) != TypedValue.TYPE_STRING) { 470 Log.w(TAG, "android:name must be string literal."); 471 return null; 472 } 473 return sa.getString(R.styleable.IntentCategory_name); 474 } finally { 475 sa.recycle(); 476 } 477 } 478 parseShareTargetAttributes(ShortcutService service, AttributeSet attrs)479 private static ShareTargetInfo parseShareTargetAttributes(ShortcutService service, 480 AttributeSet attrs) { 481 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 482 R.styleable.Intent); 483 try { 484 String targetClass = sa.getString(R.styleable.Intent_targetClass); 485 if (TextUtils.isEmpty(targetClass)) { 486 Log.w(TAG, "android:targetClass must be provided."); 487 return null; 488 } 489 return new ShareTargetInfo(null, targetClass, null); 490 } finally { 491 sa.recycle(); 492 } 493 } 494 parseShareTargetData(ShortcutService service, AttributeSet attrs)495 private static ShareTargetInfo.TargetData parseShareTargetData(ShortcutService service, 496 AttributeSet attrs) { 497 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 498 R.styleable.AndroidManifestData); 499 try { 500 if (sa.getType(R.styleable.AndroidManifestData_mimeType) != TypedValue.TYPE_STRING) { 501 Log.w(TAG, "android:mimeType must be string literal."); 502 return null; 503 } 504 String scheme = sa.getString(R.styleable.AndroidManifestData_scheme); 505 String host = sa.getString(R.styleable.AndroidManifestData_host); 506 String port = sa.getString(R.styleable.AndroidManifestData_port); 507 String path = sa.getString(R.styleable.AndroidManifestData_path); 508 String pathPattern = sa.getString(R.styleable.AndroidManifestData_pathPattern); 509 String pathPrefix = sa.getString(R.styleable.AndroidManifestData_pathPrefix); 510 String mimeType = sa.getString(R.styleable.AndroidManifestData_mimeType); 511 return new ShareTargetInfo.TargetData(scheme, host, port, path, pathPattern, pathPrefix, 512 mimeType); 513 } finally { 514 sa.recycle(); 515 } 516 } 517 } 518