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