/* * 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.intentresolver.model; 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 androidx.annotation.Nullable; import com.android.intentresolver.ResolvedComponentInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.logging.EventLog; import com.android.intentresolver.shortcuts.ScopedAppTargetListCallback; import com.google.android.collect.Lists; import java.util.ArrayList; 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}. */ public class AppPredictionServiceResolverComparator extends AbstractResolverComparator { private static final String TAG = "APSResolverComparator"; private final AppPredictor mAppPredictor; private final Context mContext; private final Map mTargetRanks = new HashMap<>(); private final Map mTargetScores = new HashMap<>(); private final UserHandle mUser; private final Intent mIntent; 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; public AppPredictionServiceResolverComparator( Context context, Intent intent, String referrerPackage, AppPredictor appPredictor, UserHandle user, EventLog eventLog, @Nullable ComponentName promoteToFirst) { super(context, intent, Lists.newArrayList(user), promoteToFirst); mContext = context; mIntent = intent; mAppPredictor = appPredictor; mUser = user; mReferrerPackage = referrerPackage; setEventLog(eventLog); mComparatorModel = buildUpdatedModel(); } @Override public int compare(ResolveInfo lhs, ResolveInfo rhs) { return mComparatorModel.getComparator().compare(lhs, rhs); } @Override public void doCompute(List targets) { if (targets.isEmpty()) { mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT); return; } List 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()); } try { mAppPredictor.sortTargets( appTargets, Executors.newSingleThreadExecutor(), new ScopedAppTargetListCallback( mContext, sortedAppTargets -> { onAppTargetsSorted(targets, sortedAppTargets); return kotlin.Unit.INSTANCE; }).toConsumer() ); } catch (IllegalStateException e) { Log.w(TAG, "Couldn't sort targets with AppPredictionService", e); } } private void onAppTargetsSorted( List targets, List sortedAppTargets) { if (sortedAppTargets.isEmpty()) { Log.i(TAG, "AppPredictionService disabled. Using resolver."); // APS for chooser is disabled. Fallback to resolver. mResolverRankerService = new ResolverRankerServiceResolverComparator( mContext, mIntent, mReferrerPackage, () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT), getEventLog(), mUser, mPromoteToFirst); mComparatorModel = buildUpdatedModel(); mResolverRankerService.compute(targets); } else { Log.i(TAG, "AppPredictionService response received"); // Skip sending to Handler which takes extra time to dispatch // messages. handleResult(sortedAppTargets); } } @Override public void handleResultMessage(Message msg) { // Null value is okay if we have defaulted to the ResolverRankerService. if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) { final List sortedAppTargets = (List) msg.obj; handleSortedAppTargets(sortedAppTargets); } else if (msg.obj == null && mResolverRankerService == null) { Log.e(TAG, "Unexpected null result"); } } private void handleResult(List sortedAppTargets) { if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) { handleSortedAppTargets(sortedAppTargets); mHandler.removeMessages(RANKER_RESULT_TIMEOUT); afterCompute(); } } private void handleSortedAppTargets(List sortedAppTargets) { if (checkAppTargetRankValid(sortedAppTargets)) { sortedAppTargets.forEach(target -> mTargetScores.put( new ComponentName(target.getPackageName(), target.getClassName()), target.getRank())); } for (int i = 0; i < sortedAppTargets.size(); i++) { ComponentName componentName = new ComponentName( sortedAppTargets.get(i).getPackageName(), sortedAppTargets.get(i).getClassName()); mTargetRanks.put(componentName, i); Log.i(TAG, "handleSortedAppTargets, sortedAppTargets #" + i + ": " + componentName); } mComparatorModel = buildUpdatedModel(); } private boolean checkAppTargetRankValid(List sortedAppTargets) { for (AppTarget target : sortedAppTargets) { if (target.getRank() != 0) { return true; } } return false; } @Override public float getScore(TargetInfo targetInfo) { return mComparatorModel.getScore(targetInfo); } @Override public void updateModel(TargetInfo targetInfo) { mComparatorModel.notifyOnTargetSelected(targetInfo); } @Override public void destroy() { 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 mTargetRanks; // Treat as immutable. AppPredictionServiceComparatorModel( AppPredictor appPredictor, @Nullable ResolverRankerServiceResolverComparator resolverRankerService, UserHandle user, Map targetRanks) { mAppPredictor = appPredictor; mResolverRankerService = resolverRankerService; mUser = user; mTargetRanks = targetRanks; } @Override public Comparator 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(TargetInfo targetInfo) { if (mResolverRankerService != null) { return mResolverRankerService.getScore(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) { if (mResolverRankerService != null) { mResolverRankerService.updateModel(targetInfo); return; } ComponentName targetComponent = targetInfo.getResolvedComponentName(); AppTargetId targetId = new AppTargetId(targetComponent.toString()); AppTarget appTarget = new AppTarget.Builder(targetId, targetComponent.getPackageName(), mUser) .setClassName(targetComponent.getClassName()) .build(); try { mAppPredictor.notifyAppTargetEvent( new AppTargetEvent.Builder(appTarget, ACTION_LAUNCH).build()); } catch (IllegalStateException e) { Log.w(TAG, "Couldn't send feedback to AppPredictionService", e); } } } }