blob: f9c0db0e9a1d225768ad1ed23c0c243b9dfd5648 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.pm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Slog;
import android.util.TypedValue;
import android.util.Xml;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class ShortcutParser {
private static final String TAG = ShortcutService.TAG;
private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE
@VisibleForTesting
static final String METADATA_KEY = "android.app.shortcuts";
private static final String TAG_SHORTCUTS = "shortcuts";
private static final String TAG_SHORTCUT = "shortcut";
private static final String TAG_INTENT = "intent";
private static final String TAG_CATEGORIES = "categories";
private static final String TAG_SHARE_TARGET = "share-target";
private static final String TAG_DATA = "data";
private static final String TAG_CATEGORY = "category";
@Nullable
public static List<ShortcutInfo> parseShortcuts(ShortcutService service, String packageName,
@UserIdInt int userId, @NonNull List<ShareTargetInfo> outShareTargets)
throws IOException, XmlPullParserException {
if (ShortcutService.DEBUG) {
Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d",
packageName, userId));
}
final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId);
if (activities == null || activities.size() == 0) {
return null;
}
List<ShortcutInfo> result = null;
outShareTargets.clear();
try {
final int size = activities.size();
for (int i = 0; i < size; i++) {
final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo;
if (activityInfoNoMetadata == null) {
continue;
}
final ActivityInfo activityInfoWithMetadata =
service.getActivityInfoWithMetadata(
activityInfoNoMetadata.getComponentName(), userId);
if (activityInfoWithMetadata != null) {
result = parseShortcutsOneFile(service, activityInfoWithMetadata, packageName,
userId, result, outShareTargets);
}
}
} catch (RuntimeException e) {
// Resource ID mismatch may cause various runtime exceptions when parsing XMLs,
// But we don't crash the device, so just swallow them.
service.wtf(
"Exception caught while parsing shortcut XML for package=" + packageName, e);
return null;
}
return result;
}
private static List<ShortcutInfo> parseShortcutsOneFile(
ShortcutService service,
ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
List<ShortcutInfo> result, @NonNull List<ShareTargetInfo> outShareTargets)
throws IOException, XmlPullParserException {
if (ShortcutService.DEBUG) {
Slog.d(TAG, String.format(
"Checking main activity %s", activityInfo.getComponentName()));
}
XmlResourceParser parser = null;
try {
parser = service.injectXmlMetaData(activityInfo, METADATA_KEY);
if (parser == null) {
return result;
}
final ComponentName activity = new ComponentName(packageName, activityInfo.name);
final AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
int rank = 0;
final int maxShortcuts = service.getMaxActivityShortcuts();
int numShortcuts = 0;
// We instantiate ShortcutInfo at <shortcut>, but we add it to the list at </shortcut>,
// after parsing <intent>. We keep the current one in here.
ShortcutInfo currentShortcut = null;
// We instantiate ShareTargetInfo at <share-target>, but add it to outShareTargets at
// </share-target>, after parsing <data> and <category>. We keep the current one here.
ShareTargetInfo currentShareTarget = null;
// Keeps parsed categories for both ShortcutInfo and ShareTargetInfo
Set<String> categories = null;
// Keeps parsed intents for ShortcutInfo
final ArrayList<Intent> intents = new ArrayList<>();
// Keeps parsed data fields for ShareTargetInfo
final ArrayList<ShareTargetInfo.TargetData> dataList = new ArrayList<>();
outer:
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) {
final int depth = parser.getDepth();
final String tag = parser.getName();
// When a shortcut tag is closing, publish.
if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) {
if (currentShortcut == null) {
// Shortcut was invalid.
continue;
}
final ShortcutInfo si = currentShortcut;
currentShortcut = null; // Make sure to null out for the next iteration.
if (si.isEnabled()) {
if (intents.size() == 0) {
Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it.");
continue;
}
} else {
// Just set the default intent to disabled shortcuts.
intents.clear();
intents.add(new Intent(Intent.ACTION_VIEW));
}
if (numShortcuts >= maxShortcuts) {
Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for "
+ activityInfo.getComponentName() + ". Skipping the rest.");
return result;
}
// Same flag as what TaskStackBuilder adds.
intents.get(0).addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_TASK_ON_HOME);
try {
si.setIntents(intents.toArray(new Intent[intents.size()]));
} catch (RuntimeException e) {
// This shouldn't happen because intents in XML can't have complicated
// extras, but just in case Intent.parseIntent() supports such a thing one
// day.
Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it.");
continue;
}
intents.clear();
if (categories != null) {
si.setCategories(categories);
categories = null;
}
if (result == null) {
result = new ArrayList<>();
}
result.add(si);
numShortcuts++;
rank++;
if (ShortcutService.DEBUG) {
Slog.d(TAG, "Shortcut added: " + si.toInsecureString());
}
continue;
}
// When a share-target tag is closing, publish.
if ((type == XmlPullParser.END_TAG) && (depth == 2)
&& (TAG_SHARE_TARGET.equals(tag))) {
if (currentShareTarget == null) {
// ShareTarget was invalid.
continue;
}
final ShareTargetInfo sti = currentShareTarget;
currentShareTarget = null; // Make sure to null out for the next iteration.
if (categories == null || categories.isEmpty() || dataList.isEmpty()) {
// Incomplete ShareTargetInfo.
continue;
}
final ShareTargetInfo newShareTarget = new ShareTargetInfo(
dataList.toArray(new ShareTargetInfo.TargetData[dataList.size()]),
sti.mTargetClass, categories.toArray(new String[categories.size()]));
outShareTargets.add(newShareTarget);
if (ShortcutService.DEBUG) {
Slog.d(TAG, "ShareTarget added: " + newShareTarget.toString());
}
categories = null;
dataList.clear();
}
// Otherwise, just look at start tags.
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth == 1 && TAG_SHORTCUTS.equals(tag)) {
continue; // Root tag.
}
if (depth == 2 && TAG_SHORTCUT.equals(tag)) {
final ShortcutInfo si = parseShortcutAttributes(
service, attrs, packageName, activity, userId, rank);
if (si == null) {
// Shortcut was invalid.
continue;
}
if (ShortcutService.DEBUG) {
Slog.d(TAG, "Shortcut found: " + si.toInsecureString());
}
if (result != null) {
for (int i = result.size() - 1; i >= 0; i--) {
if (si.getId().equals(result.get(i).getId())) {
Log.e(TAG, "Duplicate shortcut ID detected. Skipping it.");
continue outer;
}
}
}
currentShortcut = si;
categories = null;
continue;
}
if (depth == 2 && TAG_SHARE_TARGET.equals(tag)) {
final ShareTargetInfo sti = parseShareTargetAttributes(service, attrs);
if (sti == null) {
// ShareTarget was invalid.
continue;
}
currentShareTarget = sti;
categories = null;
dataList.clear();
continue;
}
if (depth == 3 && TAG_INTENT.equals(tag)) {
if ((currentShortcut == null)
|| !currentShortcut.isEnabled()) {
Log.e(TAG, "Ignoring excessive intent tag.");
continue;
}
final Intent intent = Intent.parseIntent(service.mContext.getResources(),
parser, attrs);
if (TextUtils.isEmpty(intent.getAction())) {
Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity);
currentShortcut = null; // Invalidate the current shortcut.
continue;
}
intents.add(intent);
continue;
}
if (depth == 3 && TAG_CATEGORIES.equals(tag)) {
if ((currentShortcut == null)
|| (currentShortcut.getCategories() != null)) {
continue;
}
final String name = parseCategories(service, attrs);
if (TextUtils.isEmpty(name)) {
Log.e(TAG, "Empty category found. activity=" + activity);
continue;
}
if (categories == null) {
categories = new ArraySet<>();
}
categories.add(name);
continue;
}
if (depth == 3 && TAG_CATEGORY.equals(tag)) {
if ((currentShareTarget == null)) {
continue;
}
final String name = parseCategory(service, attrs);
if (TextUtils.isEmpty(name)) {
Log.e(TAG, "Empty category found. activity=" + activity);
continue;
}
if (categories == null) {
categories = new ArraySet<>();
}
categories.add(name);
continue;
}
if (depth == 3 && TAG_DATA.equals(tag)) {
if ((currentShareTarget == null)) {
continue;
}
final ShareTargetInfo.TargetData data = parseShareTargetData(service, attrs);
if (data == null) {
Log.e(TAG, "Invalid data tag found. activity=" + activity);
continue;
}
dataList.add(data);
continue;
}
Log.w(TAG, String.format("Invalid tag '%s' found at depth %d", tag, depth));
}
} finally {
if (parser != null) {
parser.close();
}
}
return result;
}
private static String parseCategories(ShortcutService service, AttributeSet attrs) {
final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
R.styleable.ShortcutCategories);
try {
if (sa.getType(R.styleable.ShortcutCategories_name) == TypedValue.TYPE_STRING) {
return sa.getNonResourceString(R.styleable.ShortcutCategories_name);
} else {
Log.w(TAG, "android:name for shortcut category must be string literal.");
return null;
}
} finally {
sa.recycle();
}
}
private static ShortcutInfo parseShortcutAttributes(ShortcutService service,
AttributeSet attrs, String packageName, ComponentName activity,
@UserIdInt int userId, int rank) {
final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
R.styleable.Shortcut);
try {
if (sa.getType(R.styleable.Shortcut_shortcutId) != TypedValue.TYPE_STRING) {
Log.w(TAG, "android:shortcutId must be string literal. activity=" + activity);
return null;
}
final String id = sa.getNonResourceString(R.styleable.Shortcut_shortcutId);
final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true);
final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0);
final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0);
final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0);
final int disabledMessageResId = sa.getResourceId(
R.styleable.Shortcut_shortcutDisabledMessage, 0);
if (TextUtils.isEmpty(id)) {
Log.w(TAG, "android:shortcutId must be provided. activity=" + activity);
return null;
}
if (titleResId == 0) {
Log.w(TAG, "android:shortcutShortLabel must be provided. activity=" + activity);
return null;
}
return createShortcutFromManifest(
service,
userId,
id,
packageName,
activity,
titleResId,
textResId,
disabledMessageResId,
rank,
iconResId,
enabled);
} finally {
sa.recycle();
}
}
private static ShortcutInfo createShortcutFromManifest(ShortcutService service,
@UserIdInt int userId, String id, String packageName, ComponentName activityComponent,
int titleResId, int textResId, int disabledMessageResId,
int rank, int iconResId, boolean enabled) {
final int flags =
(enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED)
| ShortcutInfo.FLAG_IMMUTABLE
| ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0);
final int disabledReason =
enabled ? ShortcutInfo.DISABLED_REASON_NOT_DISABLED
: ShortcutInfo.DISABLED_REASON_BY_APP;
// Note we don't need to set resource names here yet. They'll be set when they're about
// to be published.
return new ShortcutInfo(
userId,
id,
packageName,
activityComponent,
null, // icon
null, // title string
titleResId,
null, // title res name
null, // text string
textResId,
null, // text res name
null, // disabled message string
disabledMessageResId,
null, // disabled message res name
null, // categories
null, // intent
rank,
null, // extras
service.injectCurrentTimeMillis(),
flags,
iconResId,
null, // icon res name
null, // bitmap path
disabledReason,
null /* persons */,
null /* locusId */);
}
private static String parseCategory(ShortcutService service, AttributeSet attrs) {
final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
R.styleable.IntentCategory);
try {
if (sa.getType(R.styleable.IntentCategory_name) != TypedValue.TYPE_STRING) {
Log.w(TAG, "android:name must be string literal.");
return null;
}
return sa.getString(R.styleable.IntentCategory_name);
} finally {
sa.recycle();
}
}
private static ShareTargetInfo parseShareTargetAttributes(ShortcutService service,
AttributeSet attrs) {
final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
R.styleable.Intent);
try {
String targetClass = sa.getString(R.styleable.Intent_targetClass);
if (TextUtils.isEmpty(targetClass)) {
Log.w(TAG, "android:targetClass must be provided.");
return null;
}
return new ShareTargetInfo(null, targetClass, null);
} finally {
sa.recycle();
}
}
private static ShareTargetInfo.TargetData parseShareTargetData(ShortcutService service,
AttributeSet attrs) {
final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
R.styleable.AndroidManifestData);
try {
if (sa.getType(R.styleable.AndroidManifestData_mimeType) != TypedValue.TYPE_STRING) {
Log.w(TAG, "android:mimeType must be string literal.");
return null;
}
String scheme = sa.getString(R.styleable.AndroidManifestData_scheme);
String host = sa.getString(R.styleable.AndroidManifestData_host);
String port = sa.getString(R.styleable.AndroidManifestData_port);
String path = sa.getString(R.styleable.AndroidManifestData_path);
String pathPattern = sa.getString(R.styleable.AndroidManifestData_pathPattern);
String pathPrefix = sa.getString(R.styleable.AndroidManifestData_pathPrefix);
String mimeType = sa.getString(R.styleable.AndroidManifestData_mimeType);
return new ShareTargetInfo.TargetData(scheme, host, port, path, pathPattern, pathPrefix,
mimeType);
} finally {
sa.recycle();
}
}
}