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