• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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