ShortcutParser.java revision b08790c3b2f3bdb0c2e2f7ff46e4584fb1127769
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 */ 16package com.android.server.pm; 17 18import android.annotation.Nullable; 19import android.annotation.UserIdInt; 20import android.content.ComponentName; 21import android.content.Intent; 22import android.content.pm.ActivityInfo; 23import android.content.pm.PackageInfo; 24import android.content.pm.ResolveInfo; 25import android.content.pm.ShortcutInfo; 26import android.content.res.TypedArray; 27import android.content.res.XmlResourceParser; 28import android.text.TextUtils; 29import android.util.ArraySet; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.util.Slog; 33import android.util.Xml; 34 35import com.android.internal.R; 36import com.android.internal.annotations.VisibleForTesting; 37 38import org.xmlpull.v1.XmlPullParser; 39import org.xmlpull.v1.XmlPullParserException; 40 41import java.io.IOException; 42import java.util.ArrayList; 43import java.util.List; 44import java.util.Set; 45 46public 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 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.injectGetActivityInfoWithMetadata( 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 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 131 outer: 132 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 133 && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) { 134 final int depth = parser.getDepth(); 135 final String tag = parser.getName(); 136 137 // When a shortcut tag is closing, publish. 138 if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) { 139 if (currentShortcut == null) { 140 // Shortcut was invalid. 141 continue; 142 } 143 final ShortcutInfo si = currentShortcut; 144 currentShortcut = null; // Make sure to null out for the next iteration. 145 146 if (si.getIntent() == null) { 147 Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it."); 148 continue; 149 } 150 151 if (numShortcuts >= maxShortcuts) { 152 Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for " 153 + activityInfo.getComponentName() + ". Skipping the rest."); 154 return result; 155 } 156 if (categories != null) { 157 si.setCategories(categories); 158 categories = null; 159 } 160 161 if (result == null) { 162 result = new ArrayList<>(); 163 } 164 result.add(si); 165 numShortcuts++; 166 rank++; 167 if (ShortcutService.DEBUG) { 168 Slog.d(TAG, "Shortcut added: " + si.toInsecureString()); 169 } 170 continue; 171 } 172 173 // Otherwise, just look at start tags. 174 if (type != XmlPullParser.START_TAG) { 175 continue; 176 } 177 178 if (depth == 1 && TAG_SHORTCUTS.equals(tag)) { 179 continue; // Root tag. 180 } 181 if (depth == 2 && TAG_SHORTCUT.equals(tag)) { 182 final ShortcutInfo si = parseShortcutAttributes( 183 service, attrs, packageName, activity, userId, rank); 184 if (si == null) { 185 // Shortcut was invalid. 186 continue; 187 } 188 if (ShortcutService.DEBUG) { 189 Slog.d(TAG, "Shortcut found: " + si.toInsecureString()); 190 } 191 if (result != null) { 192 for (int i = result.size() - 1; i >= 0; i--) { 193 if (si.getId().equals(result.get(i).getId())) { 194 Log.e(TAG, "Duplicate shortcut ID detected. Skipping it."); 195 continue outer; 196 } 197 } 198 } 199 if (!si.isEnabled()) { 200 // Just set the default intent to disabled shortcuts. 201 si.setIntent(new Intent(Intent.ACTION_VIEW)); 202 } 203 currentShortcut = si; 204 categories = null; 205 continue; 206 } 207 if (depth == 3 && TAG_INTENT.equals(tag)) { 208 if ((currentShortcut == null) 209 || (currentShortcut.getIntentNoExtras() != null) 210 || !currentShortcut.isEnabled()) { 211 Log.e(TAG, "Ignoring excessive intent tag."); 212 continue; 213 } 214 215 final Intent intent = Intent.parseIntent(service.mContext.getResources(), 216 parser, attrs); 217 if (TextUtils.isEmpty(intent.getAction())) { 218 Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity); 219 continue; 220 } 221 try { 222 currentShortcut.setIntent(intent); 223 } catch (RuntimeException e) { 224 // This shouldn't happen because intents in XML can't have complicated 225 // extras, but just in case Intent.parseIntent() supports such a thing one 226 // day. 227 Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it."); 228 continue; 229 } 230 continue; 231 } 232 if (depth == 3 && TAG_CATEGORIES.equals(tag)) { 233 if ((currentShortcut == null) 234 || (currentShortcut.getCategories() != null)) { 235 continue; 236 } 237 final String name = parseCategories(service, attrs); 238 if (TextUtils.isEmpty(name)) { 239 Log.e(TAG, "Empty category found. activity=" + activity); 240 continue; 241 } 242 243 if (categories == null) { 244 categories = new ArraySet<>(); 245 } 246 categories.add(name); 247 continue; 248 } 249 250 ShortcutService.warnForInvalidTag(depth, tag); 251 } 252 } finally { 253 if (parser != null) { 254 parser.close(); 255 } 256 } 257 return result; 258 } 259 260 private static String parseCategories(ShortcutService service, AttributeSet attrs) { 261 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 262 R.styleable.ShortcutCategories); 263 try { 264 return sa.getString(R.styleable.ShortcutCategories_name); 265 } finally { 266 sa.recycle(); 267 } 268 } 269 270 private static ShortcutInfo parseShortcutAttributes(ShortcutService service, 271 AttributeSet attrs, String packageName, ComponentName activity, 272 @UserIdInt int userId, int rank) { 273 final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, 274 R.styleable.Shortcut); 275 try { 276 final String id = sa.getString(R.styleable.Shortcut_shortcutId); 277 final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true); 278 final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0); 279 final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0); 280 final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0); 281 final int disabledMessageResId = sa.getResourceId( 282 R.styleable.Shortcut_shortcutDisabledMessage, 0); 283 284 if (TextUtils.isEmpty(id)) { 285 Slog.w(TAG, "Shortcut ID must be provided. activity=" + activity); 286 return null; 287 } 288 if (titleResId == 0) { 289 Slog.w(TAG, "Shortcut title must be provided. activity=" + activity); 290 return null; 291 } 292 293 return createShortcutFromManifest( 294 service, 295 userId, 296 id, 297 packageName, 298 activity, 299 titleResId, 300 textResId, 301 disabledMessageResId, 302 rank, 303 iconResId, 304 enabled); 305 } finally { 306 sa.recycle(); 307 } 308 } 309 310 private static ShortcutInfo createShortcutFromManifest(ShortcutService service, 311 @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, 312 int titleResId, int textResId, int disabledMessageResId, 313 int rank, int iconResId, boolean enabled) { 314 315 final int flags = 316 (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) 317 | ShortcutInfo.FLAG_IMMUTABLE 318 | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0); 319 320 // Note we don't need to set resource names here yet. They'll be set when they're about 321 // to be published. 322 return new ShortcutInfo( 323 userId, 324 id, 325 packageName, 326 activityComponent, 327 null, // icon 328 null, // title string 329 titleResId, 330 null, // title res name 331 null, // text string 332 textResId, 333 null, // text res name 334 null, // disabled message string 335 disabledMessageResId, 336 null, // disabled message res name 337 null, // categories 338 null, // intent 339 null, // intent extras 340 rank, 341 null, // extras 342 service.injectCurrentTimeMillis(), 343 flags, 344 iconResId, 345 null, // icon res name 346 null); // bitmap path 347 } 348} 349