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