ShortcutParser.java revision 22fcc68e6be0edaa98f3dacf79d580a5e5d50005
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            outer:
89            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
90                    && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) {
91                if (type != XmlPullParser.START_TAG) {
92                    continue;
93                }
94                final int depth = parser.getDepth();
95                final String tag = parser.getName();
96
97                if (depth == 1 && TAG_SHORTCUTS.equals(tag)) {
98                    continue; // Root tag.
99                }
100                if (depth == 2 && TAG_SHORTCUT.equals(tag)) {
101                    final ShortcutInfo si = parseShortcutAttributes(
102                            service, attrs, packageName, activity, userId);
103                    if (ShortcutService.DEBUG) {
104                        Slog.d(TAG, "Shortcut=" + si);
105                    }
106                    if (result != null) {
107                        for (int i = result.size() - 1; i >= 0; i--) {
108                            if (si.getId().equals(result.get(i).getId())) {
109                                Slog.w(TAG, "Duplicate shortcut ID detected, skipping.");
110                                continue outer;
111                            }
112                        }
113                    }
114
115                    if (si != null) {
116                        if (result == null) {
117                            result = new ArrayList<>();
118                        }
119                        result.add(si);
120                    }
121                    continue;
122                }
123                Slog.w(TAG, "Unknown tag " + tag);
124            }
125        } finally {
126            if (parser != null) {
127                parser.close();
128            }
129        }
130        return result;
131    }
132
133    private static ShortcutInfo parseShortcutAttributes(ShortcutService service,
134            AttributeSet attrs, String packageName, ComponentName activity,
135            @UserIdInt int userId) {
136        final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
137                R.styleable.Shortcut);
138        try {
139            final String id = sa.getString(R.styleable.Shortcut_shortcutId);
140            final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true);
141            final int rank = sa.getInt(R.styleable.Shortcut_shortcutRank, 0);
142            final int iconResId = sa.getResourceId(R.styleable.Shortcut_shortcutIcon, 0);
143            final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutTitle, 0);
144            final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutText, 0);
145            final int disabledMessageResId = sa.getResourceId(
146                    R.styleable.Shortcut_shortcutDisabledMessage, 0);
147            final String categories = sa.getString(R.styleable.Shortcut_shortcutCategories);
148            String intentAction = sa.getString(R.styleable.Shortcut_shortcutIntentAction);
149            final String intentData = sa.getString(R.styleable.Shortcut_shortcutIntentData);
150
151            if (TextUtils.isEmpty(id)) {
152                Slog.w(TAG, "Shortcut ID must be provided. activity=" + activity);
153                return null;
154            }
155            if (titleResId == 0) {
156                Slog.w(TAG, "Shortcut title must be provided. activity=" + activity);
157                return null;
158            }
159            if (TextUtils.isEmpty(intentAction)) {
160                if (enabled) {
161                    Slog.w(TAG, "Shortcut intent action must be provided. activity=" + activity);
162                    return null;
163                } else {
164                    // Disabled shortcut doesn't have to have an action, but just set VIEW as the
165                    // default.
166                    intentAction = Intent.ACTION_VIEW;
167                }
168            }
169
170            final ArraySet<String> categoriesSet;
171            if (categories == null) {
172                categoriesSet = null;
173            } else {
174                final String[] arr = categories.split(":");
175                categoriesSet = new ArraySet<>(arr.length);
176                for (String v : arr) {
177                    categoriesSet.add(v);
178                }
179            }
180            final Intent intent = new Intent(intentAction);
181            if (!TextUtils.isEmpty(intentData)) {
182                intent.setData(Uri.parse(intentData));
183            }
184
185            return createShortcutFromManifest(
186                    service,
187                    userId,
188                    id,
189                    packageName,
190                    activity,
191                    titleResId,
192                    textResId,
193                    disabledMessageResId,
194                    categoriesSet,
195                    intent,
196                    rank,
197                    iconResId,
198                    enabled);
199        } finally {
200            sa.recycle();
201        }
202    }
203
204    private static ShortcutInfo createShortcutFromManifest(ShortcutService service,
205            @UserIdInt int userId, String id, String packageName, ComponentName activityComponent,
206            int titleResId, int textResId, int disabledMessageResId, Set<String> categories,
207            Intent intent, int rank, int iconResId, boolean enabled) {
208
209        final int flags =
210                (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED)
211                | ShortcutInfo.FLAG_IMMUTABLE
212                | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0);
213
214        return new ShortcutInfo(
215                userId,
216                id,
217                packageName,
218                activityComponent,
219                null, // icon
220                null, // title string
221                titleResId,
222                null, // text string
223                textResId,
224                null, // disabled message string
225                disabledMessageResId,
226                categories,
227                intent,
228                null, // intent extras
229                rank,
230                null, // extras
231                service.injectCurrentTimeMillis(),
232                flags,
233                iconResId,
234                null); // bitmap path
235    }
236}
237