Fix issue #17082301: replacePreferredActivity is ignoring userId

It was being given the argument and just...  ignoring it.

But the bulk of this change is to make replacePreferredActivity
better about replacing -- it now detects if the request will not
make a change and, in that case, just do nothing.

The reason for this?

It turns out that each time you install an app, the telephony
system is calling this function over 20 times to set the default
SMS app.  This is almost always doing nothing, but before this
change it means we would re-write packages.xml over 20 times...!

There are definitely more improvements that can be made here (delaying
write of packages.xml to allow them to batch together, reducing
the amount of calls being made), but until then this is a big
improvement.

Change-Id: I02c4235b8ecd5c13ef53e65d13c7dc2223719cec
diff --git a/services/core/java/com/android/server/IntentResolver.java b/services/core/java/com/android/server/IntentResolver.java
index 64b0487..07cc864 100644
--- a/services/core/java/com/android/server/IntentResolver.java
+++ b/services/core/java/com/android/server/IntentResolver.java
@@ -69,6 +69,124 @@
         }
     }
 
+    private boolean filterEquals(IntentFilter f1, IntentFilter f2) {
+        int s1 = f1.countActions();
+        int s2 = f2.countActions();
+        if (s1 != s2) {
+            return false;
+        }
+        for (int i=0; i<s1; i++) {
+            if (!f2.hasAction(f1.getAction(i))) {
+                return false;
+            }
+        }
+        s1 = f1.countCategories();
+        s2 = f2.countCategories();
+        if (s1 != s2) {
+            return false;
+        }
+        for (int i=0; i<s1; i++) {
+            if (!f2.hasCategory(f1.getCategory(i))) {
+                return false;
+            }
+        }
+        s1 = f1.countDataTypes();
+        s2 = f2.countDataTypes();
+        if (s1 != s2) {
+            return false;
+        }
+        for (int i=0; i<s1; i++) {
+            if (!f2.hasExactDataType(f1.getDataType(i))) {
+                return false;
+            }
+        }
+        s1 = f1.countDataSchemes();
+        s2 = f2.countDataSchemes();
+        if (s1 != s2) {
+            return false;
+        }
+        for (int i=0; i<s1; i++) {
+            if (!f2.hasDataScheme(f1.getDataScheme(i))) {
+                return false;
+            }
+        }
+        s1 = f1.countDataAuthorities();
+        s2 = f2.countDataAuthorities();
+        if (s1 != s2) {
+            return false;
+        }
+        for (int i=0; i<s1; i++) {
+            if (!f2.hasDataAuthority(f1.getDataAuthority(i))) {
+                return false;
+            }
+        }
+        s1 = f1.countDataPaths();
+        s2 = f2.countDataPaths();
+        if (s1 != s2) {
+            return false;
+        }
+        for (int i=0; i<s1; i++) {
+            if (!f2.hasDataPath(f1.getDataPath(i))) {
+                return false;
+            }
+        }
+        s1 = f1.countDataSchemeSpecificParts();
+        s2 = f2.countDataSchemeSpecificParts();
+        if (s1 != s2) {
+            return false;
+        }
+        for (int i=0; i<s1; i++) {
+            if (!f2.hasDataSchemeSpecificPart(f1.getDataSchemeSpecificPart(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private ArrayList<F> collectFilters(F[] array, IntentFilter matching) {
+        ArrayList<F> res = null;
+        if (array != null) {
+            for (int i=0; i<array.length; i++) {
+                F cur = array[i];
+                if (cur == null) {
+                    break;
+                }
+                if (filterEquals(cur, matching)) {
+                    if (res == null) {
+                        res = new ArrayList<>();
+                    }
+                    res.add(cur);
+                }
+            }
+        }
+        return res;
+    }
+
+    public ArrayList<F> findFilters(IntentFilter matching) {
+        if (matching.countDataSchemes() == 1) {
+            // Fast case.
+            return collectFilters(mSchemeToFilter.get(matching.getDataScheme(0)), matching);
+        } else if (matching.countDataTypes() != 0 && matching.countActions() == 1) {
+            // Another fast case.
+            return collectFilters(mTypedActionToFilter.get(matching.getAction(0)), matching);
+        } else if (matching.countDataTypes() == 0 && matching.countDataSchemes() == 0
+                && matching.countActions() == 1) {
+            // Last fast case.
+            return collectFilters(mActionToFilter.get(matching.getAction(0)), matching);
+        } else {
+            ArrayList<F> res = null;
+            for (F cur : mFilters) {
+                if (filterEquals(cur, matching)) {
+                    if (res == null) {
+                        res = new ArrayList<>();
+                    }
+                    res.add(cur);
+                }
+            }
+            return res;
+        }
+    }
+
     public void removeFilter(F f) {
         removeFilterInternal(f);
         mFilters.remove(f);
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 89bd1d4..2d18343 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2920,7 +2920,8 @@
         findPreferredActivity(intent, resolvedType,
                 flags, query, 0, false, true, false, userId);
         // Add the new activity as the last chosen for this filter
-        addPreferredActivityInternal(filter, match, null, activity, false, userId);
+        addPreferredActivityInternal(filter, match, null, activity, false, userId,
+                "Setting last chosen");
     }
 
     @Override
@@ -11462,11 +11463,13 @@
     @Override
     public void addPreferredActivity(IntentFilter filter, int match,
             ComponentName[] set, ComponentName activity, int userId) {
-        addPreferredActivityInternal(filter, match, set, activity, true, userId);
+        addPreferredActivityInternal(filter, match, set, activity, true, userId,
+                "Adding preferred");
     }
 
     private void addPreferredActivityInternal(IntentFilter filter, int match,
-            ComponentName[] set, ComponentName activity, boolean always, int userId) {
+            ComponentName[] set, ComponentName activity, boolean always, int userId,
+            String opname) {
         // writer
         int callingUid = Binder.getCallingUid();
         enforceCrossUserPermission(callingUid, userId, true, "add preferred activity");
@@ -11488,10 +11491,11 @@
                         android.Manifest.permission.SET_PREFERRED_APPLICATIONS, null);
             }
 
-            Slog.i(TAG, "Adding preferred activity " + activity + " for user " + userId + " :");
+            PreferredIntentResolver pir = mSettings.editPreferredActivitiesLPw(userId);
+            Slog.i(TAG, opname + " activity " + activity.flattenToShortString() + " for user "
+                    + userId + ":");
             filter.dump(new LogPrinter(Log.INFO, TAG), "  ");
-            mSettings.editPreferredActivitiesLPw(userId).addFilter(
-                    new PreferredActivity(filter, match, set, activity, always));
+            pir.addFilter(new PreferredActivity(filter, match, set, activity, always));
             mSettings.writePackageRestrictionsLPr(userId);
         }
     }
@@ -11514,7 +11518,6 @@
 
         final int callingUid = Binder.getCallingUid();
         enforceCrossUserPermission(callingUid, userId, true, "replace preferred activity");
-        final int callingUserId = UserHandle.getUserId(callingUid);
         synchronized (mPackages) {
             if (mContext.checkCallingOrSelfPermission(
                     android.Manifest.permission.SET_PREFERRED_APPLICATIONS)
@@ -11529,30 +11532,61 @@
                         android.Manifest.permission.SET_PREFERRED_APPLICATIONS, null);
             }
 
-            PreferredIntentResolver pir = mSettings.mPreferredActivities.get(callingUserId);
+            PreferredIntentResolver pir = mSettings.mPreferredActivities.get(userId);
             if (pir != null) {
-                Intent intent = new Intent(filter.getAction(0)).addCategory(filter.getCategory(0));
-                if (filter.countDataSchemes() == 1) {
-                    Uri.Builder builder = new Uri.Builder();
-                    builder.scheme(filter.getDataScheme(0));
-                    intent.setData(builder.build());
-                }
-                List<PreferredActivity> matches = pir.queryIntent(
-                        intent, null, true, callingUserId);
-                if (DEBUG_PREFERRED) {
-                    Slog.i(TAG, matches.size() + " preferred matches for " + intent);
-                }
-                for (int i = 0; i < matches.size(); i++) {
-                    PreferredActivity pa = matches.get(i);
+                // Get all of the existing entries that exactly match this filter.
+                ArrayList<PreferredActivity> existing = pir.findFilters(filter);
+                if (existing != null && existing.size() == 1) {
+                    PreferredActivity cur = existing.get(0);
                     if (DEBUG_PREFERRED) {
-                        Slog.i(TAG, "Removing preferred activity "
-                                + pa.mPref.mComponent + ":");
+                        Slog.i(TAG, "Checking replace of preferred:");
                         filter.dump(new LogPrinter(Log.INFO, TAG), "  ");
+                        if (!cur.mPref.mAlways) {
+                            Slog.i(TAG, "  -- CUR; not mAlways!");
+                        } else {
+                            Slog.i(TAG, "  -- CUR: mMatch=" + cur.mPref.mMatch);
+                            Slog.i(TAG, "  -- CUR: mSet="
+                                    + Arrays.toString(cur.mPref.mSetComponents));
+                            Slog.i(TAG, "  -- CUR: mComponent=" + cur.mPref.mShortComponent);
+                            Slog.i(TAG, "  -- NEW: mMatch="
+                                    + (match&IntentFilter.MATCH_CATEGORY_MASK));
+                            Slog.i(TAG, "  -- CUR: mSet=" + Arrays.toString(set));
+                            Slog.i(TAG, "  -- CUR: mComponent=" + activity.flattenToShortString());
+                        }
+                    }
+                    if (cur.mPref.mAlways && cur.mPref.mComponent.equals(activity)
+                            && cur.mPref.mMatch == (match&IntentFilter.MATCH_CATEGORY_MASK)
+                            && cur.mPref.sameSet(set)) {
+                        if (DEBUG_PREFERRED) {
+                            Slog.i(TAG, "Replacing with same preferred activity "
+                                    + cur.mPref.mShortComponent + " for user "
+                                    + userId + ":");
+                            filter.dump(new LogPrinter(Log.INFO, TAG), "  ");
+                        } else {
+                            Slog.i(TAG, "Replacing with same preferred activity "
+                                    + cur.mPref.mShortComponent + " for user "
+                                    + userId);
+                        }
+                        return;
+                    }
+                }
+
+                if (DEBUG_PREFERRED) {
+                    Slog.i(TAG, existing.size() + " existing preferred matches for:");
+                    filter.dump(new LogPrinter(Log.INFO, TAG), "  ");
+                }
+                for (int i = 0; i < existing.size(); i++) {
+                    PreferredActivity pa = existing.get(i);
+                    if (DEBUG_PREFERRED) {
+                        Slog.i(TAG, "Removing existing preferred activity "
+                                + pa.mPref.mComponent + ":");
+                        pa.dump(new LogPrinter(Log.INFO, TAG), "  ");
                     }
                     pir.removeFilter(pa);
                 }
             }
-            addPreferredActivityInternal(filter, match, set, activity, true, callingUserId);
+            addPreferredActivityInternal(filter, match, set, activity, true, userId,
+                    "Replacing preferred");
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/PreferredComponent.java b/services/core/java/com/android/server/pm/PreferredComponent.java
index f437372..69c1909 100644
--- a/services/core/java/com/android/server/pm/PreferredComponent.java
+++ b/services/core/java/com/android/server/pm/PreferredComponent.java
@@ -44,10 +44,10 @@
     // Whether this is to be the one that's always chosen. If false, it's the most recently chosen.
     public boolean mAlways;
 
-    private final String[] mSetPackages;
-    private final String[] mSetClasses;
-    private final String[] mSetComponents;
-    private final String mShortComponent;
+    final String[] mSetPackages;
+    final String[] mSetClasses;
+    final String[] mSetComponents;
+    final String mShortComponent;
     private String mParseError;
 
     private final Callbacks mCallbacks;
@@ -193,7 +193,12 @@
     }
 
     public boolean sameSet(List<ResolveInfo> query, int priority) {
-        if (mSetPackages == null) return false;
+        if (mSetPackages == null) {
+            return query == null;
+        }
+        if (query == null) {
+            return false;
+        }
         final int NQ = query.size();
         final int NS = mSetPackages.length;
         int numMatch = 0;
@@ -215,6 +220,27 @@
         return numMatch == NS;
     }
 
+    public boolean sameSet(ComponentName[] comps) {
+        if (mSetPackages == null) return false;
+        final int NQ = comps.length;
+        final int NS = mSetPackages.length;
+        int numMatch = 0;
+        for (int i=0; i<NQ; i++) {
+            ComponentName cn = comps[i];
+            boolean good = false;
+            for (int j=0; j<NS; j++) {
+                if (mSetPackages[j].equals(cn.getPackageName())
+                        && mSetClasses[j].equals(cn.getClassName())) {
+                    numMatch++;
+                    good = true;
+                    break;
+                }
+            }
+            if (!good) return false;
+        }
+        return numMatch == NS;
+    }
+
     public void dump(PrintWriter out, String prefix, Object ident) {
         out.print(prefix); out.print(
                 Integer.toHexString(System.identityHashCode(ident)));