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