1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver.model; 18 19 import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH; 20 21 import android.annotation.Nullable; 22 import android.app.prediction.AppPredictor; 23 import android.app.prediction.AppTarget; 24 import android.app.prediction.AppTargetEvent; 25 import android.app.prediction.AppTargetId; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.ResolveInfo; 30 import android.os.Message; 31 import android.os.UserHandle; 32 import android.util.Log; 33 34 import com.android.intentresolver.logging.EventLog; 35 import com.android.intentresolver.ResolvedComponentInfo; 36 import com.android.intentresolver.chooser.TargetInfo; 37 38 import com.google.android.collect.Lists; 39 40 import java.util.ArrayList; 41 import java.util.Comparator; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.concurrent.Executors; 46 47 /** 48 * Uses an {@link AppPredictor} to sort Resolver targets. If the AppPredictionService appears to be 49 * disabled by returning an empty sorted target list, {@link AppPredictionServiceResolverComparator} 50 * will fallback to using a {@link ResolverRankerServiceResolverComparator}. 51 */ 52 public class AppPredictionServiceResolverComparator extends AbstractResolverComparator { 53 54 private static final String TAG = "APSResolverComparator"; 55 56 private final AppPredictor mAppPredictor; 57 private final Context mContext; 58 private final Map<ComponentName, Integer> mTargetRanks = new HashMap<>(); 59 private final Map<ComponentName, Integer> mTargetScores = new HashMap<>(); 60 private final UserHandle mUser; 61 private final Intent mIntent; 62 private final String mReferrerPackage; 63 // If this is non-null (and this is not destroyed), it means APS is disabled and we should fall 64 // back to using the ResolverRankerService. 65 // TODO: responsibility for this fallback behavior can live outside of the AppPrediction client. 66 private ResolverRankerServiceResolverComparator mResolverRankerService; 67 private AppPredictionServiceComparatorModel mComparatorModel; 68 AppPredictionServiceResolverComparator( Context context, Intent intent, String referrerPackage, AppPredictor appPredictor, UserHandle user, EventLog eventLog, @Nullable ComponentName promoteToFirst)69 public AppPredictionServiceResolverComparator( 70 Context context, 71 Intent intent, 72 String referrerPackage, 73 AppPredictor appPredictor, 74 UserHandle user, 75 EventLog eventLog, 76 @Nullable ComponentName promoteToFirst) { 77 super(context, intent, Lists.newArrayList(user), promoteToFirst); 78 mContext = context; 79 mIntent = intent; 80 mAppPredictor = appPredictor; 81 mUser = user; 82 mReferrerPackage = referrerPackage; 83 setEventLog(eventLog); 84 mComparatorModel = buildUpdatedModel(); 85 } 86 87 @Override compare(ResolveInfo lhs, ResolveInfo rhs)88 int compare(ResolveInfo lhs, ResolveInfo rhs) { 89 return mComparatorModel.getComparator().compare(lhs, rhs); 90 } 91 92 @Override doCompute(List<ResolvedComponentInfo> targets)93 void doCompute(List<ResolvedComponentInfo> targets) { 94 if (targets.isEmpty()) { 95 mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT); 96 return; 97 } 98 List<AppTarget> appTargets = new ArrayList<>(); 99 for (ResolvedComponentInfo target : targets) { 100 appTargets.add( 101 new AppTarget.Builder( 102 new AppTargetId(target.name.flattenToString()), 103 target.name.getPackageName(), 104 mUser) 105 .setClassName(target.name.getClassName()) 106 .build()); 107 } 108 mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(), 109 sortedAppTargets -> { 110 if (sortedAppTargets.isEmpty()) { 111 Log.i(TAG, "AppPredictionService disabled. Using resolver."); 112 // APS for chooser is disabled. Fallback to resolver. 113 mResolverRankerService = 114 new ResolverRankerServiceResolverComparator( 115 mContext, 116 mIntent, 117 mReferrerPackage, 118 () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT), 119 getEventLog(), 120 mUser, 121 mPromoteToFirst); 122 mComparatorModel = buildUpdatedModel(); 123 mResolverRankerService.compute(targets); 124 } else { 125 Log.i(TAG, "AppPredictionService response received"); 126 // Skip sending to Handler which takes extra time to dispatch messages. 127 handleResult(sortedAppTargets); 128 } 129 } 130 ); 131 } 132 133 @Override handleResultMessage(Message msg)134 void handleResultMessage(Message msg) { 135 // Null value is okay if we have defaulted to the ResolverRankerService. 136 if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) { 137 final List<AppTarget> sortedAppTargets = (List<AppTarget>) msg.obj; 138 handleSortedAppTargets(sortedAppTargets); 139 } else if (msg.obj == null && mResolverRankerService == null) { 140 Log.e(TAG, "Unexpected null result"); 141 } 142 } 143 handleResult(List<AppTarget> sortedAppTargets)144 private void handleResult(List<AppTarget> sortedAppTargets) { 145 if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) { 146 handleSortedAppTargets(sortedAppTargets); 147 mHandler.removeMessages(RANKER_RESULT_TIMEOUT); 148 afterCompute(); 149 } 150 } 151 handleSortedAppTargets(List<AppTarget> sortedAppTargets)152 private void handleSortedAppTargets(List<AppTarget> sortedAppTargets) { 153 if (checkAppTargetRankValid(sortedAppTargets)) { 154 sortedAppTargets.forEach(target -> mTargetScores.put( 155 new ComponentName(target.getPackageName(), target.getClassName()), 156 target.getRank())); 157 } 158 for (int i = 0; i < sortedAppTargets.size(); i++) { 159 ComponentName componentName = new ComponentName( 160 sortedAppTargets.get(i).getPackageName(), 161 sortedAppTargets.get(i).getClassName()); 162 mTargetRanks.put(componentName, i); 163 Log.i(TAG, "handleSortedAppTargets, sortedAppTargets #" + i + ": " + componentName); 164 } 165 mComparatorModel = buildUpdatedModel(); 166 } 167 checkAppTargetRankValid(List<AppTarget> sortedAppTargets)168 private boolean checkAppTargetRankValid(List<AppTarget> sortedAppTargets) { 169 for (AppTarget target : sortedAppTargets) { 170 if (target.getRank() != 0) { 171 return true; 172 } 173 } 174 return false; 175 } 176 177 @Override getScore(TargetInfo targetInfo)178 public float getScore(TargetInfo targetInfo) { 179 return mComparatorModel.getScore(targetInfo); 180 } 181 182 @Override updateModel(TargetInfo targetInfo)183 public void updateModel(TargetInfo targetInfo) { 184 mComparatorModel.notifyOnTargetSelected(targetInfo); 185 } 186 187 @Override destroy()188 public void destroy() { 189 if (mResolverRankerService != null) { 190 mResolverRankerService.destroy(); 191 mResolverRankerService = null; 192 mComparatorModel = buildUpdatedModel(); 193 } 194 } 195 196 /** 197 * Re-construct an {@code AppPredictionServiceComparatorModel} to replace the current model 198 * instance (if any) using the up-to-date {@code AppPredictionServiceResolverComparator} ivar 199 * values. 200 * 201 * TODO: each time we replace the model instance, we're either updating the model to use 202 * adjusted data (which is appropriate), or we're providing a (late) value for one of our ivars 203 * that wasn't available the last time the model was updated. For those latter cases, we should 204 * just avoid creating the model altogether until we have all the prerequisites we'll need. Then 205 * we can probably simplify the logic in {@code AppPredictionServiceComparatorModel} since we 206 * won't need to handle edge cases when the model data isn't fully prepared. 207 * (In some cases, these kinds of "updates" might interleave -- e.g., we might have finished 208 * initializing the first time and now want to adjust some data, but still need to wait for 209 * changes to propagate to the other ivars before rebuilding the model.) 210 */ buildUpdatedModel()211 private AppPredictionServiceComparatorModel buildUpdatedModel() { 212 return new AppPredictionServiceComparatorModel( 213 mAppPredictor, mResolverRankerService, mUser, mTargetRanks); 214 } 215 216 // TODO: Finish separating behaviors of AbstractResolverComparator, then (probably) make this a 217 // standalone class once clients are written in terms of ResolverComparatorModel. 218 static class AppPredictionServiceComparatorModel implements ResolverComparatorModel { 219 private final AppPredictor mAppPredictor; 220 private final ResolverRankerServiceResolverComparator mResolverRankerService; 221 private final UserHandle mUser; 222 private final Map<ComponentName, Integer> mTargetRanks; // Treat as immutable. 223 AppPredictionServiceComparatorModel( AppPredictor appPredictor, @Nullable ResolverRankerServiceResolverComparator resolverRankerService, UserHandle user, Map<ComponentName, Integer> targetRanks)224 AppPredictionServiceComparatorModel( 225 AppPredictor appPredictor, 226 @Nullable ResolverRankerServiceResolverComparator resolverRankerService, 227 UserHandle user, 228 Map<ComponentName, Integer> targetRanks) { 229 mAppPredictor = appPredictor; 230 mResolverRankerService = resolverRankerService; 231 mUser = user; 232 mTargetRanks = targetRanks; 233 } 234 235 @Override getComparator()236 public Comparator<ResolveInfo> getComparator() { 237 return (lhs, rhs) -> { 238 if (mResolverRankerService != null) { 239 return mResolverRankerService.compare(lhs, rhs); 240 } 241 Integer lhsRank = mTargetRanks.get(new ComponentName(lhs.activityInfo.packageName, 242 lhs.activityInfo.name)); 243 Integer rhsRank = mTargetRanks.get(new ComponentName(rhs.activityInfo.packageName, 244 rhs.activityInfo.name)); 245 if (lhsRank == null && rhsRank == null) { 246 return 0; 247 } else if (lhsRank == null) { 248 return -1; 249 } else if (rhsRank == null) { 250 return 1; 251 } 252 return lhsRank - rhsRank; 253 }; 254 } 255 256 @Override getScore(TargetInfo targetInfo)257 public float getScore(TargetInfo targetInfo) { 258 if (mResolverRankerService != null) { 259 return mResolverRankerService.getScore(targetInfo); 260 } 261 Integer rank = mTargetRanks.get(targetInfo.getResolvedComponentName()); 262 if (rank == null) { 263 Log.w(TAG, "Score requested for unknown component. Did you call compute yet?"); 264 return 0f; 265 } 266 int consecutiveSumOfRanks = (mTargetRanks.size() - 1) * (mTargetRanks.size()) / 2; 267 return 1.0f - (((float) rank) / consecutiveSumOfRanks); 268 } 269 270 @Override notifyOnTargetSelected(TargetInfo targetInfo)271 public void notifyOnTargetSelected(TargetInfo targetInfo) { 272 if (mResolverRankerService != null) { 273 mResolverRankerService.updateModel(targetInfo); 274 return; 275 } 276 ComponentName targetComponent = targetInfo.getResolvedComponentName(); 277 AppTargetId targetId = new AppTargetId(targetComponent.toString()); 278 AppTarget appTarget = 279 new AppTarget.Builder(targetId, targetComponent.getPackageName(), mUser) 280 .setClassName(targetComponent.getClassName()) 281 .build(); 282 mAppPredictor.notifyAppTargetEvent( 283 new AppTargetEvent.Builder(appTarget, ACTION_LAUNCH).build()); 284 } 285 } 286 } 287