Merge "Create ResolverComparatorModel interface." into tm-dev

This commit is contained in:
James O'Leary
2022-04-22 20:23:23 +00:00
committed by Android (Google) Code Review
8 changed files with 383 additions and 154 deletions

View File

@@ -228,12 +228,6 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
*/
abstract float getScore(ComponentName name);
/**
* Returns the list of top K component names which have highest
* {@link #getScore(ComponentName)}
*/
abstract List<ComponentName> getTopComponentNames(int topK);
/** Handles result message sent to mHandler. */
abstract void handleResultMessage(Message message);

View File

@@ -18,6 +18,7 @@ package com.android.internal.app;
import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
@@ -33,12 +34,11 @@ import android.util.Log;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
/**
* Uses an {@link AppPredictor} to sort Resolver targets. If the AppPredictionService appears to be
@@ -58,7 +58,9 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
private final String mReferrerPackage;
// If this is non-null (and this is not destroyed), it means APS is disabled and we should fall
// back to using the ResolverRankerService.
// TODO: responsibility for this fallback behavior can live outside of the AppPrediction client.
private ResolverRankerServiceResolverComparator mResolverRankerService;
private AppPredictionServiceComparatorModel mComparatorModel;
AppPredictionServiceResolverComparator(
Context context,
@@ -74,25 +76,12 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
mUser = user;
mReferrerPackage = referrerPackage;
setChooserActivityLogger(chooserActivityLogger);
mComparatorModel = buildUpdatedModel();
}
@Override
int compare(ResolveInfo lhs, ResolveInfo rhs) {
if (mResolverRankerService != null) {
return mResolverRankerService.compare(lhs, rhs);
}
Integer lhsRank = mTargetRanks.get(new ComponentName(lhs.activityInfo.packageName,
lhs.activityInfo.name));
Integer rhsRank = mTargetRanks.get(new ComponentName(rhs.activityInfo.packageName,
rhs.activityInfo.name));
if (lhsRank == null && rhsRank == null) {
return 0;
} else if (lhsRank == null) {
return -1;
} else if (rhsRank == null) {
return 1;
}
return lhsRank - rhsRank;
return mComparatorModel.getComparator().compare(lhs, rhs);
}
@Override
@@ -121,6 +110,7 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
mContext, mIntent, mReferrerPackage,
() -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
getChooserActivityLogger());
mComparatorModel = buildUpdatedModel();
mResolverRankerService.compute(targets);
} else {
Log.i(TAG, "AppPredictionService response received");
@@ -163,6 +153,7 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
mTargetRanks.put(componentName, i);
Log.i(TAG, "handleSortedAppTargets, sortedAppTargets #" + i + ": " + componentName);
}
mComparatorModel = buildUpdatedModel();
}
private boolean checkAppTargetRankValid(List<AppTarget> sortedAppTargets) {
@@ -176,43 +167,12 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
@Override
float getScore(ComponentName name) {
if (mResolverRankerService != null) {
return mResolverRankerService.getScore(name);
}
Integer rank = mTargetRanks.get(name);
if (rank == null) {
Log.w(TAG, "Score requested for unknown component. Did you call compute yet?");
return 0f;
}
int consecutiveSumOfRanks = (mTargetRanks.size() - 1) * (mTargetRanks.size()) / 2;
return 1.0f - (((float) rank) / consecutiveSumOfRanks);
}
@Override
List<ComponentName> getTopComponentNames(int topK) {
if (mResolverRankerService != null) {
return mResolverRankerService.getTopComponentNames(topK);
}
return mTargetRanks.entrySet().stream()
.sorted(Entry.comparingByValue())
.limit(topK)
.map(Entry::getKey)
.collect(Collectors.toList());
return mComparatorModel.getScore(name);
}
@Override
void updateModel(ComponentName componentName) {
if (mResolverRankerService != null) {
mResolverRankerService.updateModel(componentName);
return;
}
mAppPredictor.notifyAppTargetEvent(
new AppTargetEvent.Builder(
new AppTarget.Builder(
new AppTargetId(componentName.toString()),
componentName.getPackageName(), mUser)
.setClassName(componentName.getClassName()).build(),
ACTION_LAUNCH).build());
mComparatorModel.notifyOnTargetSelected(componentName);
}
@Override
@@ -220,6 +180,97 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
if (mResolverRankerService != null) {
mResolverRankerService.destroy();
mResolverRankerService = null;
mComparatorModel = buildUpdatedModel();
}
}
/**
* Re-construct an {@code AppPredictionServiceComparatorModel} to replace the current model
* instance (if any) using the up-to-date {@code AppPredictionServiceResolverComparator} ivar
* values.
*
* TODO: each time we replace the model instance, we're either updating the model to use
* adjusted data (which is appropriate), or we're providing a (late) value for one of our ivars
* that wasn't available the last time the model was updated. For those latter cases, we should
* just avoid creating the model altogether until we have all the prerequisites we'll need. Then
* we can probably simplify the logic in {@code AppPredictionServiceComparatorModel} since we
* won't need to handle edge cases when the model data isn't fully prepared.
* (In some cases, these kinds of "updates" might interleave -- e.g., we might have finished
* initializing the first time and now want to adjust some data, but still need to wait for
* changes to propagate to the other ivars before rebuilding the model.)
*/
private AppPredictionServiceComparatorModel buildUpdatedModel() {
return new AppPredictionServiceComparatorModel(
mAppPredictor, mResolverRankerService, mUser, mTargetRanks);
}
// TODO: Finish separating behaviors of AbstractResolverComparator, then (probably) make this a
// standalone class once clients are written in terms of ResolverComparatorModel.
static class AppPredictionServiceComparatorModel implements ResolverComparatorModel {
private final AppPredictor mAppPredictor;
private final ResolverRankerServiceResolverComparator mResolverRankerService;
private final UserHandle mUser;
private final Map<ComponentName, Integer> mTargetRanks; // Treat as immutable.
AppPredictionServiceComparatorModel(
AppPredictor appPredictor,
@Nullable ResolverRankerServiceResolverComparator resolverRankerService,
UserHandle user,
Map<ComponentName, Integer> targetRanks) {
mAppPredictor = appPredictor;
mResolverRankerService = resolverRankerService;
mUser = user;
mTargetRanks = targetRanks;
}
@Override
public Comparator<ResolveInfo> getComparator() {
return (lhs, rhs) -> {
if (mResolverRankerService != null) {
return mResolverRankerService.compare(lhs, rhs);
}
Integer lhsRank = mTargetRanks.get(new ComponentName(lhs.activityInfo.packageName,
lhs.activityInfo.name));
Integer rhsRank = mTargetRanks.get(new ComponentName(rhs.activityInfo.packageName,
rhs.activityInfo.name));
if (lhsRank == null && rhsRank == null) {
return 0;
} else if (lhsRank == null) {
return -1;
} else if (rhsRank == null) {
return 1;
}
return lhsRank - rhsRank;
};
}
@Override
public float getScore(ComponentName name) {
if (mResolverRankerService != null) {
return mResolverRankerService.getScore(name);
}
Integer rank = mTargetRanks.get(name);
if (rank == null) {
Log.w(TAG, "Score requested for unknown component. Did you call compute yet?");
return 0f;
}
int consecutiveSumOfRanks = (mTargetRanks.size() - 1) * (mTargetRanks.size()) / 2;
return 1.0f - (((float) rank) / consecutiveSumOfRanks);
}
@Override
public void notifyOnTargetSelected(ComponentName componentName) {
if (mResolverRankerService != null) {
mResolverRankerService.updateModel(componentName);
return;
}
mAppPredictor.notifyAppTargetEvent(
new AppTargetEvent.Builder(
new AppTarget.Builder(
new AppTargetId(componentName.toString()),
componentName.getPackageName(), mUser)
.setClassName(componentName.getClassName()).build(),
ACTION_LAUNCH).build());
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2022 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.content.ComponentName;
import android.content.pm.ResolveInfo;
import java.util.Comparator;
import java.util.List;
/**
* A ranking model for resolver targets, providing ordering and (optionally) numerical scoring.
*
* As required by the {@link Comparator} contract, objects returned by {@code getComparator()} must
* apply a total ordering on its inputs consistent across all calls to {@code Comparator#compare()}.
* Other query methods and ranking feedback should refer to that same ordering, so implementors are
* generally advised to "lock in" an immutable snapshot of their model data when this object is
* initialized (preferring to replace the entire {@code ResolverComparatorModel} instance if the
* backing data needs to be updated in the future).
*/
interface ResolverComparatorModel {
/**
* Get a {@code Comparator} that can be used to sort {@code ResolveInfo} targets according to
* the model ranking.
*/
Comparator<ResolveInfo> getComparator();
/**
* Get the numerical score, if any, that the model assigns to the component with the specified
* {@code name}. Scores range from zero to one, with one representing the highest possible
* likelihood that the user will select that component as the target. Implementations that don't
* assign numerical scores are <em>recommended</em> to return a value of 0 for all components.
*/
float getScore(ComponentName name);
/**
* Notify the model that the user selected a target. (Models may log this information, use it as
* a feedback signal for their ranking, etc.) Because the data in this
* {@code ResolverComparatorModel} instance is immutable, clients will need to get an up-to-date
* instance in order to see any changes in the ranking that might result from this feedback.
*/
void notifyOnTargetSelected(ComponentName componentName);
}

View File

@@ -155,14 +155,6 @@ public class ResolverListAdapter extends BaseAdapter {
return mResolverListController.getScore(componentName);
}
/**
* Returns the list of top K component names which have highest
* {@link #getScore(DisplayResolveInfo)}
*/
public List<ComponentName> getTopComponentNames(int topK) {
return mResolverListController.getTopComponentNames(topK);
}
public void updateModel(ComponentName componentName) {
mResolverListController.updateModel(componentName);
}

View File

@@ -393,14 +393,6 @@ public class ResolverListController {
return mResolverComparator.getScore(componentName);
}
/**
* Returns the list of top K component names which have highest
* {@link #getScore(DisplayResolveInfo)}
*/
public List<ComponentName> getTopComponentNames(int topK) {
return mResolverComparator.getTopComponentNames(topK);
}
public void updateModel(ComponentName componentName) {
mResolverComparator.updateModel(componentName);
}

View File

@@ -43,12 +43,12 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Ranks and compares packages based on usage stats and uses the {@link ResolverRankerService}.
@@ -83,6 +83,7 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
private ResolverRankerServiceConnection mConnection;
private Context mContext;
private CountDownLatch mConnectSignal;
private ResolverRankerServiceComparatorModel mComparatorModel;
public ResolverRankerServiceResolverComparator(Context context, Intent intent,
String referrerPackage, AfterCompute afterCompute,
@@ -99,6 +100,8 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
mRankerServiceName = new ComponentName(mContext, this.getClass());
setCallBack(afterCompute);
setChooserActivityLogger(chooserActivityLogger);
mComparatorModel = buildUpdatedModel();
}
@Override
@@ -125,6 +128,7 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
}
if (isUpdated) {
mRankerServiceName = mResolvedRankerName;
mComparatorModel = buildUpdatedModel();
}
} else {
Log.e(TAG, "Sizes of sent and received ResolverTargets diff.");
@@ -218,83 +222,25 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
}
}
predictSelectProbabilities(mTargets);
mComparatorModel = buildUpdatedModel();
}
@Override
public int compare(ResolveInfo lhs, ResolveInfo rhs) {
if (mStats != null) {
final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName(
lhs.activityInfo.packageName, lhs.activityInfo.name));
final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName(
rhs.activityInfo.packageName, rhs.activityInfo.name));
if (lhsTarget != null && rhsTarget != null) {
final int selectProbabilityDiff = Float.compare(
rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability());
if (selectProbabilityDiff != 0) {
return selectProbabilityDiff > 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());
return mComparatorModel.getComparator().compare(lhs, rhs);
}
@Override
public float getScore(ComponentName name) {
final ResolverTarget target = mTargetsDict.get(name);
if (target != null) {
return target.getSelectProbability();
}
return 0;
}
@Override
List<ComponentName> getTopComponentNames(int topK) {
return mTargetsDict.entrySet().stream()
.sorted((o1, o2) -> -Float.compare(getScore(o1.getKey()), getScore(o2.getKey())))
.limit(topK)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
return mComparatorModel.getScore(name);
}
// update ranking model when the connection to it is valid.
@Override
public void updateModel(ComponentName componentName) {
synchronized (mLock) {
if (mRanker != null) {
try {
int selectedPos = new ArrayList<ComponentName>(mTargetsDict.keySet())
.indexOf(componentName);
if (selectedPos >= 0 && mTargets != null) {
final float selectedProbability = getScore(componentName);
int order = 0;
for (ResolverTarget target : mTargets) {
if (target.getSelectProbability() > selectedProbability) {
order++;
}
}
logMetrics(order);
mRanker.train(mTargets, selectedPos);
} else {
if (DEBUG) {
Log.d(TAG, "Selected a unknown component: " + componentName);
}
}
} catch (RemoteException e) {
Log.e(TAG, "Error in Train: " + e);
}
} else {
if (DEBUG) {
Log.d(TAG, "Ranker is null; skip updateModel.");
}
}
mComparatorModel.notifyOnTargetSelected(componentName);
}
}
@@ -313,19 +259,6 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
}
}
// records metrics for evaluation.
private void logMetrics(int selectedPos) {
if (mRankerServiceName != null) {
MetricsLogger metricsLogger = new MetricsLogger();
LogMaker log = new LogMaker(MetricsEvent.ACTION_TARGET_SELECTED);
log.setComponentName(mRankerServiceName);
int isCategoryUsed = (mAnnotations == null) ? 0 : 1;
log.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, isCategoryUsed);
log.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION, selectedPos);
metricsLogger.write(log);
}
}
// connect to a ranking service.
private void initRanker(Context context) {
synchronized (mLock) {
@@ -426,6 +359,7 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
}
synchronized (mLock) {
mRanker = IResolverRankerService.Stub.asInterface(service);
mComparatorModel = buildUpdatedModel();
mConnectSignal.countDown();
}
}
@@ -443,6 +377,7 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
public void destroy() {
synchronized (mLock) {
mRanker = null;
mComparatorModel = buildUpdatedModel();
}
}
}
@@ -453,6 +388,7 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
mTargetsDict.clear();
mTargets = null;
mRankerServiceName = new ComponentName(mContext, this.getClass());
mComparatorModel = buildUpdatedModel();
mResolvedRankerName = null;
initRanker(mContext);
}
@@ -508,4 +444,155 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
}
return false;
}
/**
* Re-construct a {@code ResolverRankerServiceComparatorModel} to replace the current model
* instance (if any) using the up-to-date {@code ResolverRankerServiceResolverComparator} ivar
* values.
*
* TODO: each time we replace the model instance, we're either updating the model to use
* adjusted data (which is appropriate), or we're providing a (late) value for one of our ivars
* that wasn't available the last time the model was updated. For those latter cases, we should
* just avoid creating the model altogether until we have all the prerequisites we'll need. Then
* we can probably simplify the logic in {@code ResolverRankerServiceComparatorModel} since we
* won't need to handle edge cases when the model data isn't fully prepared.
* (In some cases, these kinds of "updates" might interleave -- e.g., we might have finished
* initializing the first time and now want to adjust some data, but still need to wait for
* changes to propagate to the other ivars before rebuilding the model.)
*/
private ResolverRankerServiceComparatorModel buildUpdatedModel() {
// TODO: we don't currently guarantee that the underlying target list/map won't be mutated,
// so the ResolverComparatorModel may provide inconsistent results. We should make immutable
// copies of the data (waiting for any necessary remaining data before creating the model).
return new ResolverRankerServiceComparatorModel(
mStats,
mTargetsDict,
mTargets,
mCollator,
mRanker,
mRankerServiceName,
(mAnnotations != null),
mPm);
}
/**
* Implementation of a {@code ResolverComparatorModel} that provides the same ranking logic as
* the legacy {@code ResolverRankerServiceResolverComparator}, as a refactoring step toward
* removing the complex legacy API.
*/
static class ResolverRankerServiceComparatorModel implements ResolverComparatorModel {
private final Map<String, UsageStats> mStats; // Treat as immutable.
private final Map<ComponentName, ResolverTarget> mTargetsDict; // Treat as immutable.
private final List<ResolverTarget> mTargets; // Treat as immutable.
private final Collator mCollator;
private final IResolverRankerService mRanker;
private final ComponentName mRankerServiceName;
private final boolean mAnnotationsUsed;
private final PackageManager mPm;
// TODO: it doesn't look like we should have to pass both targets and targetsDict, but it's
// not written in a way that makes it clear whether we can derive one from the other (at
// least in this constructor).
ResolverRankerServiceComparatorModel(
Map<String, UsageStats> stats,
Map<ComponentName, ResolverTarget> targetsDict,
List<ResolverTarget> targets,
Collator collator,
IResolverRankerService ranker,
ComponentName rankerServiceName,
boolean annotationsUsed,
PackageManager pm) {
mStats = stats;
mTargetsDict = targetsDict;
mTargets = targets;
mCollator = collator;
mRanker = ranker;
mRankerServiceName = rankerServiceName;
mAnnotationsUsed = annotationsUsed;
mPm = pm;
}
@Override
public Comparator<ResolveInfo> getComparator() {
// TODO: doCompute() doesn't seem to be concerned about null-checking mStats. Is that
// a bug there, or do we have a way of knowing it will be non-null under certain
// conditions?
return (lhs, rhs) -> {
if (mStats != null) {
final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName(
lhs.activityInfo.packageName, lhs.activityInfo.name));
final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName(
rhs.activityInfo.packageName, rhs.activityInfo.name));
if (lhsTarget != null && rhsTarget != null) {
final int selectProbabilityDiff = Float.compare(
rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability());
if (selectProbabilityDiff != 0) {
return selectProbabilityDiff > 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());
};
}
@Override
public float getScore(ComponentName name) {
final ResolverTarget target = mTargetsDict.get(name);
if (target != null) {
return target.getSelectProbability();
}
return 0;
}
@Override
public void notifyOnTargetSelected(ComponentName componentName) {
if (mRanker != null) {
try {
int selectedPos = new ArrayList<ComponentName>(mTargetsDict.keySet())
.indexOf(componentName);
if (selectedPos >= 0 && mTargets != null) {
final float selectedProbability = getScore(componentName);
int order = 0;
for (ResolverTarget target : mTargets) {
if (target.getSelectProbability() > selectedProbability) {
order++;
}
}
logMetrics(order);
mRanker.train(mTargets, selectedPos);
} else {
if (DEBUG) {
Log.d(TAG, "Selected a unknown component: " + componentName);
}
}
} catch (RemoteException e) {
Log.e(TAG, "Error in Train: " + e);
}
} else {
if (DEBUG) {
Log.d(TAG, "Ranker is null; skip updateModel.");
}
}
}
/** Records metrics for evaluation. */
private void logMetrics(int selectedPos) {
if (mRankerServiceName != null) {
MetricsLogger metricsLogger = new MetricsLogger();
LogMaker log = new LogMaker(MetricsEvent.ACTION_TARGET_SELECTED);
log.setComponentName(mRankerServiceName);
log.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, mAnnotationsUsed);
log.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION, selectedPos);
metricsLogger.write(log);
}
}
}
}

View File

@@ -115,11 +115,6 @@ public class AbstractResolverComparatorTest {
@Override
void handleResultMessage(Message message) {}
@Override
List<ComponentName> getTopComponentNames(int topK) {
return null;
}
};
return testComparator;
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2022 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.content.ComponentName;
import android.content.pm.ResolveInfo;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Basic {@link ResolverComparatorModel} implementation that sorts according to a pre-defined (or
* default) {@link java.util.Comparator}.
*/
public class FakeResolverComparatorModel implements ResolverComparatorModel {
private final Comparator<ResolveInfo> mComparator;
public static FakeResolverComparatorModel makeModelFromComparator(
Comparator<ResolveInfo> comparator) {
return new FakeResolverComparatorModel(comparator);
}
public static FakeResolverComparatorModel makeDefaultModel() {
return makeModelFromComparator(Comparator.comparing(ri -> ri.activityInfo.name));
}
@Override
public Comparator<ResolveInfo> getComparator() {
return mComparator;
}
@Override
public float getScore(ComponentName name) {
return 0.0f; // Models are not required to provide numerical scores.
}
@Override
public void notifyOnTargetSelected(ComponentName componentName) {
System.out.println(
"User selected " + componentName + " under model " + System.identityHashCode(this));
}
private FakeResolverComparatorModel(Comparator<ResolveInfo> comparator) {
mComparator = comparator;
}
}