blob: d2fdc65b2c36761074f0d1be8225a8d12cb968fa [file] [log] [blame]
/*
* Copyright 2018 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 static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.TargetInfo;
import com.google.android.collect.Lists;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
/**
* Uses an {@link AppPredictor} to sort Resolver targets. If the AppPredictionService appears to be
* disabled by returning an empty sorted target list, {@link AppPredictionServiceResolverComparator}
* will fallback to using a {@link ResolverRankerServiceResolverComparator}.
*/
class AppPredictionServiceResolverComparator extends AbstractResolverComparator {
private static final String TAG = "APSResolverComparator";
private final AppPredictor mAppPredictor;
private final Context mContext;
private final UserHandle mUser;
private final Intent mIntent;
private final String mReferrerPackage;
private final ModelBuilder mModelBuilder;
private ResolverComparatorModel mComparatorModel;
private ResolverAppPredictorCallback mSortingCallback;
// 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;
AppPredictionServiceResolverComparator(
Context context,
Intent intent,
String referrerPackage,
AppPredictor appPredictor,
UserHandle user,
ChooserActivityLogger chooserActivityLogger) {
super(context, intent, Lists.newArrayList(user));
mContext = context;
mIntent = intent;
mAppPredictor = appPredictor;
mUser = user;
mReferrerPackage = referrerPackage;
setChooserActivityLogger(chooserActivityLogger);
mModelBuilder = new ModelBuilder(appPredictor, user);
mComparatorModel = mModelBuilder.buildFromRankedList(Collections.emptyList());
}
@Override
void destroy() {
if (mResolverRankerService != null) {
mResolverRankerService.destroy();
mResolverRankerService = null;
// TODO: may not be necessary to build a new model, since we're destroying anyways.
mComparatorModel = mModelBuilder.buildFallbackModel(mResolverRankerService);
}
if (mSortingCallback != null) {
mSortingCallback.destroy();
}
}
@Override
int compare(ResolveInfo lhs, ResolveInfo rhs) {
return mComparatorModel.getComparator().compare(lhs, rhs);
}
@Override
float getScore(TargetInfo targetInfo) {
return mComparatorModel.getScore(targetInfo);
}
@Override
void updateModel(TargetInfo targetInfo) {
mComparatorModel.notifyOnTargetSelected(targetInfo);
}
@Override
void handleResultMessage(Message msg) {
// Null value is okay if we have defaulted to the ResolverRankerService.
if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) {
// TODO: this probably never happens? The sorting callback circumvents the Handler
// design to call handleResult() directly instead of sending the list through a Message.
// (OK to leave as-is since the Handler design is going away soon.)
mComparatorModel = mModelBuilder.buildFromRankedList((List<AppTarget>) msg.obj);
} else if (msg.obj == null && mResolverRankerService == null) {
Log.e(TAG, "Unexpected null result");
}
}
@Override
void doCompute(List<ResolvedComponentInfo> targets) {
if (targets.isEmpty()) {
mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT);
return;
}
List<AppTarget> appTargets = new ArrayList<>();
for (ResolvedComponentInfo target : targets) {
appTargets.add(
new AppTarget.Builder(
new AppTargetId(target.name.flattenToString()),
target.name.getPackageName(),
mUser)
.setClassName(target.name.getClassName())
.build());
}
if (mSortingCallback != null) {
mSortingCallback.destroy();
}
mSortingCallback = new ResolverAppPredictorCallback(sortedAppTargets -> {
if (sortedAppTargets.isEmpty()) {
Log.i(TAG, "AppPredictionService disabled. Using resolver.");
setupFallbackModel(targets);
} else {
Log.i(TAG, "AppPredictionService response received");
// Skip sending to Handler which takes extra time to dispatch messages.
// TODO: the Handler guards some concurrency conditions, so this could
// probably result in a race (we're not currently on the Handler thread?).
// We'll leave this as-is since we intend to remove the Handler design
// shortly, but this is still an unsound shortcut.
handleResult(sortedAppTargets);
}
});
mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(),
mSortingCallback.asConsumer());
}
private void setupFallbackModel(List<ResolvedComponentInfo> targets) {
mResolverRankerService =
new ResolverRankerServiceResolverComparator(
mContext,
mIntent,
mReferrerPackage,
() -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
getChooserActivityLogger(),
mUser);
mComparatorModel = mModelBuilder.buildFallbackModel(mResolverRankerService);
mResolverRankerService.compute(targets);
}
private void handleResult(List<AppTarget> sortedAppTargets) {
if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) {
mComparatorModel = mModelBuilder.buildFromRankedList(sortedAppTargets);
mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
afterCompute();
}
}
static class ModelBuilder {
private final AppPredictor mAppPredictor;
private final UserHandle mUser;
ModelBuilder(AppPredictor appPredictor, UserHandle user) {
mAppPredictor = appPredictor;
mUser = user;
}
ResolverComparatorModel buildFromRankedList(List<AppTarget> sortedAppTargets) {
return new AppPredictionServiceComparatorModel(
mAppPredictor, mUser, buildTargetRanksMapFromSortedTargets(sortedAppTargets));
}
ResolverComparatorModel buildFallbackModel(
ResolverRankerServiceResolverComparator fallback) {
return adaptLegacyResolverComparatorToComparatorModel(fallback);
}
// The remaining methods would be static if this weren't an inner class (i.e., they don't
// depend on any ivars, not even the ones in ModelBuilder).
private Map<ComponentName, Integer> buildTargetRanksMapFromSortedTargets(
List<AppTarget> sortedAppTargets) {
Map<ComponentName, Integer> targetRanks = new HashMap<>();
for (int i = 0; i < sortedAppTargets.size(); i++) {
ComponentName componentName = new ComponentName(
sortedAppTargets.get(i).getPackageName(),
sortedAppTargets.get(i).getClassName());
targetRanks.put(componentName, i);
Log.i(TAG, "handleSortedAppTargets, sortedAppTargets #" + i + ": " + componentName);
}
return targetRanks;
}
// TODO: when the refactoring is further along we'll probably have access to the
// comparator's new ResolverComparatorModel API, so we won't have to adapt from the legacy
// interface here. On the other hand, AppPredictionServiceResolverComparatorModel (or its
// replacement counterpart) shouldn't still be responsible for implementing the
// ResolverRankerService fallback logic at that time.
private ResolverComparatorModel adaptLegacyResolverComparatorToComparatorModel(
AbstractResolverComparator comparator) {
return new ResolverComparatorModel() {
@Override
public Comparator<ResolveInfo> getComparator() {
// Adapt the base type, which doesn't declare itself to be an implementation
// of {@code Comparator<ResolveInfo>} even though it has the one method.
return (lhs, rhs) -> comparator.compare(lhs, rhs);
}
@Override
public float getScore(TargetInfo targetInfo) {
return comparator.getScore(targetInfo);
}
@Override
public void notifyOnTargetSelected(TargetInfo targetInfo) {
comparator.updateModel(targetInfo);
}
};
}
}
// 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 UserHandle mUser;
private final Map<ComponentName, Integer> mTargetRanks; // Treat as immutable.
AppPredictionServiceComparatorModel(
AppPredictor appPredictor,
UserHandle user,
Map<ComponentName, Integer> targetRanks) {
mAppPredictor = appPredictor;
mUser = user;
mTargetRanks = targetRanks;
}
@Override
public Comparator<ResolveInfo> getComparator() {
return (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(TargetInfo targetInfo) {
Integer rank = mTargetRanks.get(targetInfo.getResolvedComponentName());
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(TargetInfo targetInfo) {
mAppPredictor.notifyAppTargetEvent(
new AppTargetEvent.Builder(
new AppTarget.Builder(
new AppTargetId(targetInfo.getResolvedComponentName().toString()),
targetInfo.getResolvedComponentName().getPackageName(), mUser)
.setClassName(targetInfo.getResolvedComponentName()
.getClassName()).build(),
ACTION_LAUNCH).build());
}
}
}