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.TypedValue;
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.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
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
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
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
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
341        // Note we don't need to set resource names here yet.  They'll be set when they're about
342        // to be published.
343        return new ShortcutInfo(
344                userId,
345                id,
346                packageName,
347                activityComponent,
348                null, // icon
349                null, // title string
350                titleResId,
351                null, // title res name
352                null, // text string
353                textResId,
354                null, // text res name
355                null, // disabled message string
356                disabledMessageResId,
357                null, // disabled message res name
358                null, // categories
359                null, // intent
360                rank,
361                null, // extras
362                service.injectCurrentTimeMillis(),
363                flags,
364                iconResId,
365                null, // icon res name
366                null); // bitmap path
367    }
368}
369