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