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