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