blob: 03a3a3821f6a92181c077002c464057cb41d3e4f [file] [log] [blame]
/*
* 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 = false;
// One week
private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 7;
private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12;
private static final float RECENCY_MULTIPLIER = 2.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;
}
}
final boolean lPinned = lhsp.isPinned();
final boolean rPinned = rhsp.isPinned();
if (lPinned && !rPinned) {
return -1;
} else if (!lPinned && rPinned) {
return 1;
}
// Pinned items stay stable within a normal lexical sort and ignore scoring.
if (!lPinned && !rPinned) {
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());
}
public float getScore(ComponentName name) {
final ScoredTarget target = mScoredTargets.get(name);
if (target != null) {
return target.score;
}
return 0;
}
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
+ "}";
}
}
}