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