Merge "Start using some better sorting for intent resolution" into mnc-dev
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index ba4af89..39c86f9 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -18,8 +18,6 @@
 
 import android.app.Activity;
 import android.app.ActivityThread;
-import android.app.usage.UsageStats;
-import android.app.usage.UsageStatsManager;
 import android.os.AsyncTask;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -64,14 +62,11 @@
 import android.widget.Toast;
 import com.android.internal.widget.ResolverDrawerLayout;
 
-import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
@@ -100,10 +95,7 @@
     private boolean mResolvingHome = false;
     private int mProfileSwitchMessageId = -1;
     private final ArrayList<Intent> mIntents = new ArrayList<>();
-
-    private UsageStatsManager mUsm;
-    private Map<String, UsageStats> mStats;
-    private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14;
+    private ResolverComparator mResolverComparator;
 
     private boolean mRegistered;
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
@@ -222,10 +214,6 @@
         }
 
         mPm = getPackageManager();
-        mUsm = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
-
-        final long sinceTime = System.currentTimeMillis() - USAGE_STATS_PERIOD;
-        mStats = mUsm.queryAndAggregateUsageStats(sinceTime, System.currentTimeMillis());
 
         mPackageMonitor.register(this, getMainLooper(), false);
         mRegistered = true;
@@ -236,6 +224,10 @@
         // Add our initial intent as the first item, regardless of what else has already been added.
         mIntents.add(0, new Intent(intent));
 
+        final String referrerPackage = getReferrerPackageName();
+
+        mResolverComparator = new ResolverComparator(this, getTargetIntent(), referrerPackage);
+
         configureContentView(mIntents, initialIntents, rList, alwaysUseOption);
 
         // Prevent the Resolver window from becoming the top fullscreen window and thus from taking
@@ -265,7 +257,6 @@
             // Try to initialize the title icon if we have a view for it and a title to match
             final ImageView titleIcon = (ImageView) findViewById(R.id.title_icon);
             if (titleIcon != null) {
-                final String referrerPackage = getReferrerPackageName();
                 ApplicationInfo ai = null;
                 try {
                     if (!TextUtils.isEmpty(referrerPackage)) {
@@ -1175,8 +1166,8 @@
                     }
                 }
                 if (N > 1) {
-                    Collections.sort(currentResolveList,
-                            new ResolverComparator(ResolverActivity.this, getTargetIntent()));
+                    mResolverComparator.compute(currentResolveList);
+                    Collections.sort(currentResolveList, mResolverComparator);
                 }
                 // First put the initial items at the top.
                 if (mInitialIntents != null) {
@@ -1651,63 +1642,4 @@
                 && match <= IntentFilter.MATCH_CATEGORY_PATH;
     }
 
-    class ResolverComparator implements Comparator<ResolvedComponentInfo> {
-        private final Collator mCollator;
-        private final boolean mHttp;
-
-        public ResolverComparator(Context context, Intent intent) {
-            mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
-            String scheme = intent.getScheme();
-            mHttp = "http".equals(scheme) || "https".equals(scheme);
-        }
-
-        @Override
-        public int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) {
-            final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
-            final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
-
-            // We want to put the one targeted to another user at the end of the dialog.
-            if (lhs.targetUserId != UserHandle.USER_CURRENT) {
-                return 1;
-            }
-
-            if (mHttp) {
-                // Special case: we want filters that match URI paths/schemes to be
-                // ordered before others.  This is for the case when opening URIs,
-                // to make native apps go above browsers.
-                final boolean lhsSpecific = isSpecificUriMatch(lhs.match);
-                final boolean rhsSpecific = isSpecificUriMatch(rhs.match);
-                if (lhsSpecific != rhsSpecific) {
-                    return lhsSpecific ? -1 : 1;
-                }
-            }
-
-            if (mStats != null) {
-                final long timeDiff =
-                        getPackageTimeSpent(rhs.activityInfo.packageName) -
-                        getPackageTimeSpent(lhs.activityInfo.packageName);
-
-                if (timeDiff != 0) {
-                    return timeDiff > 0 ? 1 : -1;
-                }
-            }
-
-            CharSequence  sa = lhs.loadLabel(mPm);
-            if (sa == null) sa = lhs.activityInfo.name;
-            CharSequence  sb = rhs.loadLabel(mPm);
-            if (sb == null) sb = rhs.activityInfo.name;
-
-            return mCollator.compare(sa.toString(), sb.toString());
-        }
-
-        private long getPackageTimeSpent(String packageName) {
-            if (mStats != null) {
-                final UsageStats stats = mStats.get(packageName);
-                if (stats != null) {
-                    return stats.getTotalTimeInForeground();
-                }
-            }
-            return 0;
-        }
-    }
 }
diff --git a/core/java/com/android/internal/app/ResolverComparator.java b/core/java/com/android/internal/app/ResolverComparator.java
new file mode 100644
index 0000000..42668f1
--- /dev/null
+++ b/core/java/com/android/internal/app/ResolverComparator.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2015 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.internal.app;
+
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Ranks and compares packages based on usage stats.
+ */
+class ResolverComparator implements Comparator<ResolvedComponentInfo> {
+    private static final String TAG = "ResolverComparator";
+
+    private static final boolean DEBUG = true;
+
+    // Two weeks
+    private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14;
+
+    private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12;
+
+    private static final float RECENCY_MULTIPLIER = 3.f;
+
+    private final Collator mCollator;
+    private final boolean mHttp;
+    private final PackageManager mPm;
+    private final UsageStatsManager mUsm;
+    private final Map<String, UsageStats> mStats;
+    private final long mCurrentTime;
+    private final long mSinceTime;
+    private final LinkedHashMap<ComponentName, ScoredTarget> mScoredTargets = new LinkedHashMap<>();
+    private final String mReferrerPackage;
+
+    public ResolverComparator(Context context, Intent intent, String referrerPackage) {
+        mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
+        String scheme = intent.getScheme();
+        mHttp = "http".equals(scheme) || "https".equals(scheme);
+        mReferrerPackage = referrerPackage;
+
+        mPm = context.getPackageManager();
+        mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
+
+        mCurrentTime = System.currentTimeMillis();
+        mSinceTime = mCurrentTime - USAGE_STATS_PERIOD;
+        mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime);
+    }
+
+    public void compute(List<ResolvedComponentInfo> targets) {
+        mScoredTargets.clear();
+
+        final long recentSinceTime = mCurrentTime - RECENCY_TIME_PERIOD;
+
+        long mostRecentlyUsedTime = recentSinceTime + 1;
+        long mostTimeSpent = 1;
+        int mostLaunched = 1;
+
+        for (ResolvedComponentInfo target : targets) {
+            final ScoredTarget scoredTarget
+                    = new ScoredTarget(target.getResolveInfoAt(0).activityInfo);
+            mScoredTargets.put(target.name, scoredTarget);
+            final UsageStats pkStats = mStats.get(target.name.getPackageName());
+            if (pkStats != null) {
+                // Only count recency for apps that weren't the caller
+                // since the caller is always the most recent.
+                // Persistent processes muck this up, so omit them too.
+                if (!target.name.getPackageName().equals(mReferrerPackage)
+                        && !isPersistentProcess(target)) {
+                    final long lastTimeUsed = pkStats.getLastTimeUsed();
+                    scoredTarget.lastTimeUsed = lastTimeUsed;
+                    if (lastTimeUsed > mostRecentlyUsedTime) {
+                        mostRecentlyUsedTime = lastTimeUsed;
+                    }
+                }
+                final long timeSpent = pkStats.getTotalTimeInForeground();
+                scoredTarget.timeSpent = timeSpent;
+                if (timeSpent > mostTimeSpent) {
+                    mostTimeSpent = timeSpent;
+                }
+                final int launched = pkStats.mLaunchCount;
+                scoredTarget.launchCount = launched;
+                if (launched > mostLaunched) {
+                    mostLaunched = launched;
+                }
+            }
+        }
+
+
+        if (DEBUG) {
+            Log.d(TAG, "compute - mostRecentlyUsedTime: " + mostRecentlyUsedTime
+                    + " mostTimeSpent: " + mostTimeSpent
+                    + " recentSinceTime: " + recentSinceTime
+                    + " mostLaunched: " + mostLaunched);
+        }
+
+        for (ScoredTarget target : mScoredTargets.values()) {
+            final float recency = (float) Math.max(target.lastTimeUsed - recentSinceTime, 0)
+                    / (mostRecentlyUsedTime - recentSinceTime);
+            final float recencyScore = recency * recency * RECENCY_MULTIPLIER;
+            final float usageTimeScore = (float) target.timeSpent / mostTimeSpent;
+            final float launchCountScore = (float) target.launchCount / mostLaunched;
+
+            target.score = recencyScore + usageTimeScore + launchCountScore;
+            if (DEBUG) {
+                Log.d(TAG, "Scores: recencyScore: " + recencyScore
+                        + " usageTimeScore: " + usageTimeScore
+                        + " launchCountScore: " + launchCountScore
+                        + " - " + target);
+            }
+        }
+    }
+
+    static boolean isPersistentProcess(ResolvedComponentInfo rci) {
+        if (rci != null && rci.getCount() > 0) {
+            return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
+                    ApplicationInfo.FLAG_PERSISTENT) != 0;
+        }
+        return false;
+    }
+
+    @Override
+    public int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) {
+        final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
+        final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
+
+        // We want to put the one targeted to another user at the end of the dialog.
+        if (lhs.targetUserId != UserHandle.USER_CURRENT) {
+            return 1;
+        }
+
+        if (mHttp) {
+            // Special case: we want filters that match URI paths/schemes to be
+            // ordered before others.  This is for the case when opening URIs,
+            // to make native apps go above browsers.
+            final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match);
+            final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match);
+            if (lhsSpecific != rhsSpecific) {
+                return lhsSpecific ? -1 : 1;
+            }
+        }
+
+        if (mStats != null) {
+            final ScoredTarget lhsTarget = mScoredTargets.get(new ComponentName(
+                    lhs.activityInfo.packageName, lhs.activityInfo.name));
+            final ScoredTarget rhsTarget = mScoredTargets.get(new ComponentName(
+                    rhs.activityInfo.packageName, rhs.activityInfo.name));
+            final float diff = rhsTarget.score - lhsTarget.score;
+
+            if (diff != 0) {
+                return diff > 0 ? 1 : -1;
+            }
+        }
+
+        CharSequence  sa = lhs.loadLabel(mPm);
+        if (sa == null) sa = lhs.activityInfo.name;
+        CharSequence  sb = rhs.loadLabel(mPm);
+        if (sb == null) sb = rhs.activityInfo.name;
+
+        return mCollator.compare(sa.toString().trim(), sb.toString().trim());
+    }
+
+    static class ScoredTarget {
+        public final ComponentInfo componentInfo;
+        public float score;
+        public long lastTimeUsed;
+        public long timeSpent;
+        public long launchCount;
+
+        public ScoredTarget(ComponentInfo ci) {
+            componentInfo = ci;
+        }
+
+        @Override
+        public String toString() {
+            return "ScoredTarget{" + componentInfo
+                    + " score: " + score
+                    + " lastTimeUsed: " + lastTimeUsed
+                    + " timeSpent: " + timeSpent
+                    + " launchCount: " + launchCount
+                    + "}";
+        }
+    }
+}