Parses share targets from shortcuts.xml
Test: atest com.android.server.pm.ShortcutManagerTest1
Test: atest com.android.server.pm.ShortcutManagerTest2
Test: atest com.android.server.pm.ShortcutManagerTest3
Test: atest com.android.server.pm.ShortcutManagerTest4
Test: atest com.android.server.pm.ShortcutManagerTest5
Test: atest com.android.server.pm.ShortcutManagerTest6
Test: atest com.android.server.pm.ShortcutManagerTest7
Test: atest com.android.server.pm.ShortcutManagerTest8
Test: atest com.android.server.pm.ShortcutManagerTest9
Test: atest com.android.server.pm.ShortcutManagerTest10
Test: atest CtsShortcutHostTestCases CtsShortcutManagerTestCases
Change-Id: I1ddcd4e689f5d76d68b5068629cbe2c35d0a2841
diff --git a/services/core/java/com/android/server/pm/ShareTargetInfo.java b/services/core/java/com/android/server/pm/ShareTargetInfo.java
new file mode 100644
index 0000000..9e8b73e
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShareTargetInfo.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018 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.text.TextUtils;
+
+/**
+ * Represents a Share Target definition, read from the application's manifest (shortcuts.xml)
+ */
+class ShareTargetInfo {
+ static class TargetData {
+ final String mScheme;
+ final String mHost;
+ final String mPort;
+ final String mPath;
+ final String mPathPattern;
+ final String mPathPrefix;
+ final String mMimeType;
+
+ TargetData(String scheme, String host, String port, String path, String pathPattern,
+ String pathPrefix, String mimeType) {
+ mScheme = scheme;
+ mHost = host;
+ mPort = port;
+ mPath = path;
+ mPathPattern = pathPattern;
+ mPathPrefix = pathPrefix;
+ mMimeType = mimeType;
+ }
+
+ public void toStringInner(StringBuilder strBuilder) {
+ if (!TextUtils.isEmpty(mScheme)) {
+ strBuilder.append(" scheme=").append(mScheme);
+ }
+ if (!TextUtils.isEmpty(mHost)) {
+ strBuilder.append(" host=").append(mHost);
+ }
+ if (!TextUtils.isEmpty(mPort)) {
+ strBuilder.append(" port=").append(mPort);
+ }
+ if (!TextUtils.isEmpty(mPath)) {
+ strBuilder.append(" path=").append(mPath);
+ }
+ if (!TextUtils.isEmpty(mPathPattern)) {
+ strBuilder.append(" pathPattern=").append(mPathPattern);
+ }
+ if (!TextUtils.isEmpty(mPathPrefix)) {
+ strBuilder.append(" pathPrefix=").append(mPathPrefix);
+ }
+ if (!TextUtils.isEmpty(mMimeType)) {
+ strBuilder.append(" mimeType=").append(mMimeType);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder strBuilder = new StringBuilder();
+ toStringInner(strBuilder);
+ return strBuilder.toString();
+ }
+ }
+
+ final TargetData[] mTargetData;
+ final String mTargetClass;
+ final String[] mCategories;
+
+ ShareTargetInfo(TargetData[] data, String targetClass, String[] categories) {
+ mTargetData = data;
+ mTargetClass = targetClass;
+ mCategories = categories;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder strBuilder = new StringBuilder();
+ strBuilder.append("targetClass=").append(mTargetClass);
+ for (int i = 0; i < mTargetData.length; i++) {
+ strBuilder.append(" data={");
+ mTargetData[i].toStringInner(strBuilder);
+ strBuilder.append("}");
+ }
+ for (int i = 0; i < mCategories.length; i++) {
+ strBuilder.append(" category=").append(mCategories[i]);
+ }
+
+ return strBuilder.toString();
+ }
+}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 92e261a..83f0fde 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -111,6 +111,11 @@
final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
/**
+ * All the share targets from the package
+ */
+ private final ArrayList<ShareTargetInfo> mShareTargets = new ArrayList<>(0);
+
+ /**
* # of times the package has called rate-limited APIs.
*/
private int mApiCallCount;
@@ -739,15 +744,16 @@
List<ShortcutInfo> newManifestShortcutList = null;
try {
newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService,
- getPackageName(), getPackageUserId());
+ getPackageName(), getPackageUserId(), mShareTargets);
} catch (IOException|XmlPullParserException e) {
Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e);
}
final int manifestShortcutSize = newManifestShortcutList == null ? 0
: newManifestShortcutList.size();
if (ShortcutService.DEBUG) {
- Slog.d(TAG, String.format("Package %s has %d manifest shortcut(s)",
- getPackageName(), manifestShortcutSize));
+ Slog.d(TAG,
+ String.format("Package %s has %d manifest shortcut(s), and %d share target(s)",
+ getPackageName(), manifestShortcutSize, mShareTargets.size()));
}
if (isNewApp && (manifestShortcutSize == 0)) {
// If it's a new app, and it doesn't have manifest shortcuts, then nothing to do.
@@ -1657,6 +1663,11 @@
return new ArrayList<>(mShortcuts.values());
}
+ @VisibleForTesting
+ List<ShareTargetInfo> getAllShareTargetsForTest() {
+ return new ArrayList<>(mShareTargets);
+ }
+
@Override
public void verifyStates() {
super.verifyStates();
diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java
index 866c46c..90f08c3 100644
--- a/services/core/java/com/android/server/pm/ShortcutParser.java
+++ b/services/core/java/com/android/server/pm/ShortcutParser.java
@@ -15,6 +15,7 @@
*/
package com.android.server.pm;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
@@ -55,10 +56,14 @@
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) throws IOException, XmlPullParserException {
+ 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));
@@ -69,6 +74,7 @@
}
List<ShortcutInfo> result = null;
+ outShareTargets.clear();
try {
final int size = activities.size();
@@ -82,8 +88,8 @@
service.getActivityInfoWithMetadata(
activityInfoNoMetadata.getComponentName(), userId);
if (activityInfoWithMetadata != null) {
- result = parseShortcutsOneFile(
- service, activityInfoWithMetadata, packageName, userId, result);
+ result = parseShortcutsOneFile(service, activityInfoWithMetadata, packageName,
+ userId, result, outShareTargets);
}
}
} catch (RuntimeException e) {
@@ -99,7 +105,8 @@
private static List<ShortcutInfo> parseShortcutsOneFile(
ShortcutService service,
ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
- List<ShortcutInfo> result) throws IOException, XmlPullParserException {
+ List<ShortcutInfo> result, @NonNull List<ShareTargetInfo> outShareTargets)
+ throws IOException, XmlPullParserException {
if (ShortcutService.DEBUG) {
Slog.d(TAG, String.format(
"Checking main activity %s", activityInfo.getComponentName()));
@@ -126,9 +133,19 @@
// 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)) {
@@ -194,6 +211,32 @@
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;
@@ -224,6 +267,17 @@
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()) {
@@ -258,6 +312,34 @@
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));
}
@@ -369,4 +451,57 @@
null, // bitmap path
disabledReason);
}
+
+ 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();
+ }
+ }
}
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index b9c3048..2b773f4 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -41,8 +41,8 @@
import android.content.pm.LauncherApps.ShortcutQuery;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
@@ -98,8 +98,8 @@
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.Preconditions;
-import com.android.server.LocalServices;
import com.android.internal.util.StatLogger;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.ShortcutUser.PackageWithUser;
@@ -1172,7 +1172,7 @@
return true;
}
}
-
+
// If the local copy says the user is locked, check with AM for the actual state, since
// the user might just have been unlocked.
// Note we just don't use isUserUnlockingOrUnlocked() here, because it'll return false