ResolverComparator.java revision 064139434da2d7a790bb0e7ea377dc176cd9a6ee
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18package com.android.internal.app; 19 20import android.app.usage.UsageStats; 21import android.app.usage.UsageStatsManager; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentFilter; 26import android.content.pm.ApplicationInfo; 27import android.content.pm.ComponentInfo; 28import android.content.pm.PackageManager; 29import android.content.pm.ResolveInfo; 30import android.os.UserHandle; 31import android.text.TextUtils; 32import android.util.Log; 33import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; 34 35import java.text.Collator; 36import java.util.ArrayList; 37import java.util.Comparator; 38import java.util.LinkedHashMap; 39import java.util.List; 40import java.util.Map; 41 42/** 43 * Ranks and compares packages based on usage stats. 44 */ 45class ResolverComparator implements Comparator<ResolvedComponentInfo> { 46 private static final String TAG = "ResolverComparator"; 47 48 private static final boolean DEBUG = false; 49 50 // Two weeks 51 private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14; 52 53 private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12; 54 55 private static final float RECENCY_MULTIPLIER = 3.f; 56 57 private final Collator mCollator; 58 private final boolean mHttp; 59 private final PackageManager mPm; 60 private final UsageStatsManager mUsm; 61 private final Map<String, UsageStats> mStats; 62 private final long mCurrentTime; 63 private final long mSinceTime; 64 private final LinkedHashMap<ComponentName, ScoredTarget> mScoredTargets = new LinkedHashMap<>(); 65 private final String mReferrerPackage; 66 67 public ResolverComparator(Context context, Intent intent, String referrerPackage) { 68 mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); 69 String scheme = intent.getScheme(); 70 mHttp = "http".equals(scheme) || "https".equals(scheme); 71 mReferrerPackage = referrerPackage; 72 73 mPm = context.getPackageManager(); 74 mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); 75 76 mCurrentTime = System.currentTimeMillis(); 77 mSinceTime = mCurrentTime - USAGE_STATS_PERIOD; 78 mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime); 79 } 80 81 public void compute(List<ResolvedComponentInfo> targets) { 82 mScoredTargets.clear(); 83 84 final long recentSinceTime = mCurrentTime - RECENCY_TIME_PERIOD; 85 86 long mostRecentlyUsedTime = recentSinceTime + 1; 87 long mostTimeSpent = 1; 88 int mostLaunched = 1; 89 90 for (ResolvedComponentInfo target : targets) { 91 final ScoredTarget scoredTarget 92 = new ScoredTarget(target.getResolveInfoAt(0).activityInfo); 93 mScoredTargets.put(target.name, scoredTarget); 94 final UsageStats pkStats = mStats.get(target.name.getPackageName()); 95 if (pkStats != null) { 96 // Only count recency for apps that weren't the caller 97 // since the caller is always the most recent. 98 // Persistent processes muck this up, so omit them too. 99 if (!target.name.getPackageName().equals(mReferrerPackage) 100 && !isPersistentProcess(target)) { 101 final long lastTimeUsed = pkStats.getLastTimeUsed(); 102 scoredTarget.lastTimeUsed = lastTimeUsed; 103 if (lastTimeUsed > mostRecentlyUsedTime) { 104 mostRecentlyUsedTime = lastTimeUsed; 105 } 106 } 107 final long timeSpent = pkStats.getTotalTimeInForeground(); 108 scoredTarget.timeSpent = timeSpent; 109 if (timeSpent > mostTimeSpent) { 110 mostTimeSpent = timeSpent; 111 } 112 final int launched = pkStats.mLaunchCount; 113 scoredTarget.launchCount = launched; 114 if (launched > mostLaunched) { 115 mostLaunched = launched; 116 } 117 } 118 } 119 120 121 if (DEBUG) { 122 Log.d(TAG, "compute - mostRecentlyUsedTime: " + mostRecentlyUsedTime 123 + " mostTimeSpent: " + mostTimeSpent 124 + " recentSinceTime: " + recentSinceTime 125 + " mostLaunched: " + mostLaunched); 126 } 127 128 for (ScoredTarget target : mScoredTargets.values()) { 129 final float recency = (float) Math.max(target.lastTimeUsed - recentSinceTime, 0) 130 / (mostRecentlyUsedTime - recentSinceTime); 131 final float recencyScore = recency * recency * RECENCY_MULTIPLIER; 132 final float usageTimeScore = (float) target.timeSpent / mostTimeSpent; 133 final float launchCountScore = (float) target.launchCount / mostLaunched; 134 135 target.score = recencyScore + usageTimeScore + launchCountScore; 136 if (DEBUG) { 137 Log.d(TAG, "Scores: recencyScore: " + recencyScore 138 + " usageTimeScore: " + usageTimeScore 139 + " launchCountScore: " + launchCountScore 140 + " - " + target); 141 } 142 } 143 } 144 145 static boolean isPersistentProcess(ResolvedComponentInfo rci) { 146 if (rci != null && rci.getCount() > 0) { 147 return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags & 148 ApplicationInfo.FLAG_PERSISTENT) != 0; 149 } 150 return false; 151 } 152 153 @Override 154 public int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) { 155 final ResolveInfo lhs = lhsp.getResolveInfoAt(0); 156 final ResolveInfo rhs = rhsp.getResolveInfoAt(0); 157 158 // We want to put the one targeted to another user at the end of the dialog. 159 if (lhs.targetUserId != UserHandle.USER_CURRENT) { 160 return 1; 161 } 162 163 if (mHttp) { 164 // Special case: we want filters that match URI paths/schemes to be 165 // ordered before others. This is for the case when opening URIs, 166 // to make native apps go above browsers. 167 final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match); 168 final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match); 169 if (lhsSpecific != rhsSpecific) { 170 return lhsSpecific ? -1 : 1; 171 } 172 } 173 174 if (mStats != null) { 175 final ScoredTarget lhsTarget = mScoredTargets.get(new ComponentName( 176 lhs.activityInfo.packageName, lhs.activityInfo.name)); 177 final ScoredTarget rhsTarget = mScoredTargets.get(new ComponentName( 178 rhs.activityInfo.packageName, rhs.activityInfo.name)); 179 final float diff = rhsTarget.score - lhsTarget.score; 180 181 if (diff != 0) { 182 return diff > 0 ? 1 : -1; 183 } 184 } 185 186 CharSequence sa = lhs.loadLabel(mPm); 187 if (sa == null) sa = lhs.activityInfo.name; 188 CharSequence sb = rhs.loadLabel(mPm); 189 if (sb == null) sb = rhs.activityInfo.name; 190 191 return mCollator.compare(sa.toString().trim(), sb.toString().trim()); 192 } 193 194 static class ScoredTarget { 195 public final ComponentInfo componentInfo; 196 public float score; 197 public long lastTimeUsed; 198 public long timeSpent; 199 public long launchCount; 200 201 public ScoredTarget(ComponentInfo ci) { 202 componentInfo = ci; 203 } 204 205 @Override 206 public String toString() { 207 return "ScoredTarget{" + componentInfo 208 + " score: " + score 209 + " lastTimeUsed: " + lastTimeUsed 210 + " timeSpent: " + timeSpent 211 + " launchCount: " + launchCount 212 + "}"; 213 } 214 } 215} 216