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