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