/* * 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 { 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 mStats; private final long mCurrentTime; private final long mSinceTime; private final LinkedHashMap 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 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 + "}"; } } }