1 /*
2  * Copyright 2021 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  */
16 
17 package androidx.core.content.pm;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.content.res.XmlResourceParser;
27 import android.os.Bundle;
28 import android.util.Log;
29 
30 import androidx.annotation.RestrictTo;
31 import androidx.annotation.VisibleForTesting;
32 import androidx.annotation.WorkerThread;
33 
34 import org.jspecify.annotations.NonNull;
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.IOException;
39 import java.util.ArrayList;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Set;
43 
44 /**
45  * Parses information of static shortcuts from shortcuts.xml
46  */
47 @RestrictTo(LIBRARY_GROUP)
48 public class ShortcutXmlParser {
49 
50     private static final String TAG = "ShortcutXmlParser";
51 
52     private static final String META_DATA_APP_SHORTCUTS = "android.app.shortcuts";
53     private static final String TAG_SHORTCUT = "shortcut";
54     private static final String ATTR_SHORTCUT_ID = "shortcutId";
55 
56     // List of static shortcuts loaded from app's manifest. Will not change while the app is
57     // running.
58     private static volatile ArrayList<String> sShortcutIds;
59     private static final Object GET_INSTANCE_LOCK = new Object();
60 
61     /**
62      * Returns a singleton instance of list of ids of static shortcuts parsed from shortcuts.xml
63      */
64     @WorkerThread
getShortcutIds(final @NonNull Context context)65     public static @NonNull List<String> getShortcutIds(final @NonNull Context context) {
66         if (sShortcutIds == null) {
67             synchronized (GET_INSTANCE_LOCK) {
68                 if (sShortcutIds == null) {
69                     sShortcutIds = new ArrayList<>();
70                     sShortcutIds.addAll(parseShortcutIds(context));
71                 }
72             }
73         }
74         return sShortcutIds;
75     }
76 
ShortcutXmlParser()77     private ShortcutXmlParser() {
78         /* Hide the constructor */
79     }
80 
81     /**
82      * Parses the shortcut ids of static shortcuts from the calling package.
83      * Calling package is determined by {@link Context#getPackageName}
84      * Returns a set of string which contains the ids of static shortcuts.
85      */
86     @SuppressWarnings("deprecation")
parseShortcutIds(final @NonNull Context context)87     private static @NonNull Set<String> parseShortcutIds(final @NonNull Context context) {
88         final Set<String> result = new HashSet<>();
89         final Intent mainIntent = new Intent(Intent.ACTION_MAIN);
90         mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
91         mainIntent.setPackage(context.getPackageName());
92 
93         final List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(
94                 mainIntent, PackageManager.GET_META_DATA);
95         if (resolveInfos == null || resolveInfos.size() == 0) {
96             return result;
97         }
98         try {
99             for (ResolveInfo info : resolveInfos) {
100                 final ActivityInfo activityInfo = info.activityInfo;
101                 final Bundle metaData = activityInfo.metaData;
102                 if (metaData != null && metaData.containsKey(META_DATA_APP_SHORTCUTS)) {
103                     try (XmlResourceParser parser = getXmlResourceParser(context, activityInfo)) {
104                         result.addAll(parseShortcutIds(parser));
105                     }
106                 }
107             }
108         } catch (Exception e) {
109             // Resource ID mismatch may cause various runtime exceptions when parsing XMLs,
110             // But we don't crash the device, so just swallow them.
111             Log.e(TAG, "Failed to parse the Xml resource: ", e);
112         }
113         return result;
114     }
115 
getXmlResourceParser(Context context, ActivityInfo info)116     private static @NonNull XmlResourceParser getXmlResourceParser(Context context,
117             ActivityInfo info) {
118         final XmlResourceParser parser = info.loadXmlMetaData(context.getPackageManager(),
119                 META_DATA_APP_SHORTCUTS);
120         if (parser == null) {
121             throw new IllegalArgumentException("Failed to open " + META_DATA_APP_SHORTCUTS
122                     + " meta-data resource of " + info.name);
123         }
124 
125         return parser;
126     }
127 
128     /**
129      * Parses the shortcut ids from given XmlPullParser.
130      */
131     @VisibleForTesting
parseShortcutIds(final @NonNull XmlPullParser parser)132     public static @NonNull List<String> parseShortcutIds(final @NonNull XmlPullParser parser)
133             throws IOException, XmlPullParserException {
134 
135         final List<String> result = new ArrayList<>(1);
136         int type;
137 
138         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
139                 && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) {
140             final int depth = parser.getDepth();
141             final String tag = parser.getName();
142 
143             if ((type == XmlPullParser.START_TAG) && (depth == 2) && TAG_SHORTCUT.equals(tag)) {
144                 final String shortcutId = getAttributeValue(
145                         parser, ATTR_SHORTCUT_ID);
146                 if (shortcutId == null) {
147                     continue;
148                 }
149                 result.add(shortcutId);
150             }
151         }
152 
153         return result;
154     }
155 
getAttributeValue(XmlPullParser parser, String attribute)156     private static String getAttributeValue(XmlPullParser parser, String attribute) {
157         String value = parser.getAttributeValue("http://schemas.android.com/apk/res/android",
158                 attribute);
159         if (value == null) {
160             value = parser.getAttributeValue(null, attribute);
161         }
162         return value;
163     }
164 }
165