ShortcutParser.java revision 157b1628fd84dc3ef0355fddd8d281618f94d33e
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.net.Uri;
28import android.text.TextUtils;
29import android.util.ArraySet;
30import android.util.AttributeSet;
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.pm.Shortcuts";
52
53    private static final String TAG_SHORTCUTS = "shortcuts";
54    private static final String TAG_SHORTCUT = "shortcut";
55
56    @Nullable
57    public static List<ShortcutInfo> parseShortcuts(ShortcutService service,
58            String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException {
59        final PackageInfo pi = service.injectGetActivitiesWithMetadata(packageName, userId);
60
61        List<ShortcutInfo> result = null;
62
63        if (pi != null && pi.activities != null) {
64            for (ActivityInfo activityInfo : pi.activities) {
65                result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result);
66            }
67        }
68        return result;
69    }
70
71    private static List<ShortcutInfo> parseShortcutsOneFile(
72            ShortcutService service,
73            ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
74            List<ShortcutInfo> result) throws IOException, XmlPullParserException {
75        XmlResourceParser parser = null;
76        try {
77            parser = service.injectXmlMetaData(activityInfo, METADATA_KEY);
78            if (parser == null) {
79                return result;
80            }
81
82            final ComponentName activity = new ComponentName(packageName, activityInfo.name);
83
84            final AttributeSet attrs = Xml.asAttributeSet(parser);
85
86            int type;
87
88            int rank = 0;
89            final int maxShortcuts = service.getMaxActivityShortcuts();
90            int numShortcuts = 0;
91
92            outer:
93            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
94                    && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) {
95                if (type != XmlPullParser.START_TAG) {
96                    continue;
97                }
98                final int depth = parser.getDepth();
99                final String tag = parser.getName();
100
101                if (depth == 1 && TAG_SHORTCUTS.equals(tag)) {
102                    continue; // Root tag.
103                }
104                if (depth == 2 && TAG_SHORTCUT.equals(tag)) {
105                    final ShortcutInfo si = parseShortcutAttributes(
106                            service, attrs, packageName, activity, userId, rank++);
107                    if (ShortcutService.DEBUG) {
108                        Slog.d(TAG, "Shortcut=" + si);
109                    }
110                    if (result != null) {
111                        for (int i = result.size() - 1; i >= 0; i--) {
112                            if (si.getId().equals(result.get(i).getId())) {
113                                Slog.w(TAG, "Duplicate shortcut ID detected, skipping.");
114                                continue outer;
115                            }
116                        }
117                    }
118
119                    if (si != null) {
120                        if (numShortcuts >= maxShortcuts) {
121                            Slog.w(TAG, "More than " + maxShortcuts + " shortcuts found for "
122                                    + activityInfo.getComponentName() + ", ignoring the rest.");
123                            return result;
124                        }
125
126                        if (result == null) {
127                            result = new ArrayList<>();
128                        }
129                        result.add(si);
130                        numShortcuts++;
131                    }
132                    continue;
133                }
134                Slog.w(TAG, "Unknown tag " + tag);
135            }
136        } finally {
137            if (parser != null) {
138                parser.close();
139            }
140        }
141        return result;
142    }
143
144    private static ShortcutInfo parseShortcutAttributes(ShortcutService service,
145            AttributeSet attrs, String packageName, ComponentName activity,
146            @UserIdInt int userId, int rank) {
147        final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
148                R.styleable.Shortcut);
149        try {
150            final String id = sa.getString(R.styleable.Shortcut_shortcutId);
151            final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true);
152            final int iconResId = sa.getResourceId(R.styleable.Shortcut_shortcutIcon, 0);
153            final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0);
154            final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0);
155            final int disabledMessageResId = sa.getResourceId(
156                    R.styleable.Shortcut_shortcutDisabledMessage, 0);
157            final String categories = sa.getString(R.styleable.Shortcut_shortcutCategories);
158            String intentAction = sa.getString(R.styleable.Shortcut_shortcutIntentAction);
159            final String intentData = sa.getString(R.styleable.Shortcut_shortcutIntentData);
160
161            if (TextUtils.isEmpty(id)) {
162                Slog.w(TAG, "Shortcut ID must be provided. activity=" + activity);
163                return null;
164            }
165            if (titleResId == 0) {
166                Slog.w(TAG, "Shortcut title must be provided. activity=" + activity);
167                return null;
168            }
169            if (TextUtils.isEmpty(intentAction)) {
170                if (enabled) {
171                    Slog.w(TAG, "Shortcut intent action must be provided. activity=" + activity);
172                    return null;
173                } else {
174                    // Disabled shortcut doesn't have to have an action, but just set VIEW as the
175                    // default.
176                    intentAction = Intent.ACTION_VIEW;
177                }
178            }
179
180            final ArraySet<String> categoriesSet;
181            if (categories == null) {
182                categoriesSet = null;
183            } else {
184                final String[] arr = categories.split(":");
185                categoriesSet = new ArraySet<>(arr.length);
186                for (String v : arr) {
187                    categoriesSet.add(v);
188                }
189            }
190            final Intent intent = new Intent(intentAction);
191            if (!TextUtils.isEmpty(intentData)) {
192                intent.setData(Uri.parse(intentData));
193            }
194
195            return createShortcutFromManifest(
196                    service,
197                    userId,
198                    id,
199                    packageName,
200                    activity,
201                    titleResId,
202                    textResId,
203                    disabledMessageResId,
204                    categoriesSet,
205                    intent,
206                    rank,
207                    iconResId,
208                    enabled);
209        } finally {
210            sa.recycle();
211        }
212    }
213
214    private static ShortcutInfo createShortcutFromManifest(ShortcutService service,
215            @UserIdInt int userId, String id, String packageName, ComponentName activityComponent,
216            int titleResId, int textResId, int disabledMessageResId, Set<String> categories,
217            Intent intent, int rank, int iconResId, boolean enabled) {
218
219        final int flags =
220                (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED)
221                | ShortcutInfo.FLAG_IMMUTABLE
222                | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0);
223
224        // Note we don't need to set resource names here yet.  They'll be set when they're about
225        // to be published.
226        return new ShortcutInfo(
227                userId,
228                id,
229                packageName,
230                activityComponent,
231                null, // icon
232                null, // title string
233                titleResId,
234                null, // title res name
235                null, // text string
236                textResId,
237                null, // text res name
238                null, // disabled message string
239                disabledMessageResId,
240                null, // disabled message res name
241                categories,
242                intent,
243                null, // intent extras
244                rank,
245                null, // extras
246                service.injectCurrentTimeMillis(),
247                flags,
248                iconResId,
249                null, // icon res name
250                null); // bitmap path
251    }
252}
253