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 387 if (TextUtils.isEmpty(id)) { 388 Log.w(TAG, "android:shortcutId must be provided. activity=" + activity); 389 return null; 390 } 391 if (titleResId == 0) { 392 Log.w(TAG, "android:shortcutShortLabel must be provided. activity=" + activity); 393 return null; 394 } 395 396 return createShortcutFromManifest( 397 service, 398 userId, 399 id, 400 packageName, 401 activity, 402 titleResId, 403 textResId, 404 disabledMessageResId, 405 rank, 406 iconResId, 407 enabled); 408 } finally { 409 sa.recycle(); 410 } 411 } 412 createShortcutFromManifest(ShortcutService service, @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, int titleResId, int textResId, int disabledMessageResId, int rank, int iconResId, boolean enabled)413 private static ShortcutInfo createShortcutFromManifest(ShortcutService service, 414 @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, 415 int titleResId, int textResId, int disabledMessageResId, 416 int rank, int iconResId, boolean enabled) { 417 418 final int flags = 419 (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) 420 | ShortcutInfo.FLAG_IMMUTABLE 421 | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0); 422 final int disabledReason = 423 enabled ? ShortcutInfo.DISABLED_REASON_NOT_DISABLED 424 : ShortcutInfo.DISABLED_REASON_BY_APP; 425 426 // Note we don't need to set resource names here yet. They'll be set when they're about 427 // to be published. 428 return new ShortcutInfo( 429 userId, 430 id, 431 packageName, 432 activityComponent, 433 null, // icon 434 null, // title string 435 titleResId, 436 null, // title res name 437 null, // text string 438 textResId, 439 null, // text res name 440 null, // disabled message string 441 disabledMessageResId, 442 null, // disabled message res name 443 null, // categories 444 null, // intent 445 rank, 446 null, // extras 447 service.injectCurrentTimeMillis(), 448 flags, 449 iconResId, 450 null, // icon res name 451 null, // bitmap path 452 disabledReason, 453 null /* persons */, 454 null /* locusId */); 455 } 456 parseCategory(ShortcutService service, AttributeSet attrs)457 private static String parseCategory(ShortcutService service, AttributeSet attrs) { 458 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 459 R.styleable.IntentCategory); 460 try { 461 if (sa.getType(R.styleable.IntentCategory_name) != TypedValue.TYPE_STRING) { 462 Log.w(TAG, "android:name must be string literal."); 463 return null; 464 } 465 return sa.getString(R.styleable.IntentCategory_name); 466 } finally { 467 sa.recycle(); 468 } 469 } 470 parseShareTargetAttributes(ShortcutService service, AttributeSet attrs)471 private static ShareTargetInfo parseShareTargetAttributes(ShortcutService service, 472 AttributeSet attrs) { 473 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 474 R.styleable.Intent); 475 try { 476 String targetClass = sa.getString(R.styleable.Intent_targetClass); 477 if (TextUtils.isEmpty(targetClass)) { 478 Log.w(TAG, "android:targetClass must be provided."); 479 return null; 480 } 481 return new ShareTargetInfo(null, targetClass, null); 482 } finally { 483 sa.recycle(); 484 } 485 } 486 parseShareTargetData(ShortcutService service, AttributeSet attrs)487 private static ShareTargetInfo.TargetData parseShareTargetData(ShortcutService service, 488 AttributeSet attrs) { 489 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 490 R.styleable.AndroidManifestData); 491 try { 492 if (sa.getType(R.styleable.AndroidManifestData_mimeType) != TypedValue.TYPE_STRING) { 493 Log.w(TAG, "android:mimeType must be string literal."); 494 return null; 495 } 496 String scheme = sa.getString(R.styleable.AndroidManifestData_scheme); 497 String host = sa.getString(R.styleable.AndroidManifestData_host); 498 String port = sa.getString(R.styleable.AndroidManifestData_port); 499 String path = sa.getString(R.styleable.AndroidManifestData_path); 500 String pathPattern = sa.getString(R.styleable.AndroidManifestData_pathPattern); 501 String pathPrefix = sa.getString(R.styleable.AndroidManifestData_pathPrefix); 502 String mimeType = sa.getString(R.styleable.AndroidManifestData_mimeType); 503 return new ShareTargetInfo.TargetData(scheme, host, port, path, pathPattern, pathPrefix, 504 mimeType); 505 } finally { 506 sa.recycle(); 507 } 508 } 509 } 510