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