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