blob: bb7e4d5114ba8be7fe49d844cc5ea1e005843b4c [file] [log] [blame]
George Hodulikc681ce42019-04-12 17:10:31 -07001/*
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
George Hodulik30f0c6f2019-03-20 18:23:23 -070017package com.android.internal.app;
18
George Hodulik93562442019-03-21 17:35:23 -070019import android.app.usage.UsageStatsManager;
George Hodulik30f0c6f2019-03-20 18:23:23 -070020import android.content.ComponentName;
George Hodulik93562442019-03-21 17:35:23 -070021import android.content.Context;
22import android.content.Intent;
23import android.content.pm.PackageManager;
24import android.content.pm.ResolveInfo;
George Hodulik1f5d9bf2019-04-29 14:55:48 -070025import android.os.Handler;
26import android.os.Looper;
27import android.os.Message;
George Hodulik93562442019-03-21 17:35:23 -070028import android.os.UserHandle;
29import android.util.Log;
George Hodulik1f5d9bf2019-04-29 14:55:48 -070030
George Hodulik30f0c6f2019-03-20 18:23:23 -070031import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
George Hodulik1f5d9bf2019-04-29 14:55:48 -070032
Alison Cichowlas1fd47152019-11-14 19:50:55 -050033import java.text.Collator;
George Hodulik93562442019-03-21 17:35:23 -070034import java.util.ArrayList;
George Hodulik30f0c6f2019-03-20 18:23:23 -070035import java.util.Comparator;
36import java.util.List;
37
38/**
39 * Used to sort resolved activities in {@link ResolverListController}.
Alison Cichowlas1fd47152019-11-14 19:50:55 -050040 *
41 * @hide
George Hodulik30f0c6f2019-03-20 18:23:23 -070042 */
Alison Cichowlas1fd47152019-11-14 19:50:55 -050043public abstract class AbstractResolverComparator implements Comparator<ResolvedComponentInfo> {
George Hodulik30f0c6f2019-03-20 18:23:23 -070044
George Hodulik93562442019-03-21 17:35:23 -070045 private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3;
George Hodulik1f5d9bf2019-04-29 14:55:48 -070046 private static final boolean DEBUG = false;
47 private static final String TAG = "AbstractResolverComp";
George Hodulik93562442019-03-21 17:35:23 -070048
George Hodulik3f399f22019-04-26 16:17:54 -070049 protected AfterCompute mAfterCompute;
George Hodulik93562442019-03-21 17:35:23 -070050 protected final PackageManager mPm;
51 protected final UsageStatsManager mUsm;
52 protected String[] mAnnotations;
53 protected String mContentType;
54
55 // True if the current share is a link.
56 private final boolean mHttp;
57 // can be null if mHttp == false or current user has no default browser package
58 private final String mDefaultBrowserPackageName;
59
George Hodulik1f5d9bf2019-04-29 14:55:48 -070060 // message types
61 static final int RANKER_SERVICE_RESULT = 0;
62 static final int RANKER_RESULT_TIMEOUT = 1;
63
64 // timeout for establishing connections with a ResolverRankerService, collecting features and
65 // predicting ranking scores.
66 private static final int WATCHDOG_TIMEOUT_MILLIS = 500;
67
Alison Cichowlas1fd47152019-11-14 19:50:55 -050068 private final Comparator<ResolveInfo> mAzComparator;
69
George Hodulik1f5d9bf2019-04-29 14:55:48 -070070 protected final Handler mHandler = new Handler(Looper.getMainLooper()) {
71 public void handleMessage(Message msg) {
72 switch (msg.what) {
73 case RANKER_SERVICE_RESULT:
74 if (DEBUG) {
75 Log.d(TAG, "RANKER_SERVICE_RESULT");
76 }
77 if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) {
George Hodulik3f399f22019-04-26 16:17:54 -070078 handleResultMessage(msg);
George Hodulik1f5d9bf2019-04-29 14:55:48 -070079 mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
80 afterCompute();
81 }
82 break;
83
84 case RANKER_RESULT_TIMEOUT:
85 if (DEBUG) {
86 Log.d(TAG, "RANKER_RESULT_TIMEOUT; unbinding services");
87 }
88 mHandler.removeMessages(RANKER_SERVICE_RESULT);
89 afterCompute();
90 break;
91
92 default:
93 super.handleMessage(msg);
94 }
95 }
96 };
97
Alison Cichowlas1fd47152019-11-14 19:50:55 -050098 public AbstractResolverComparator(Context context, Intent intent) {
George Hodulik93562442019-03-21 17:35:23 -070099 String scheme = intent.getScheme();
100 mHttp = "http".equals(scheme) || "https".equals(scheme);
101 mContentType = intent.getType();
102 getContentAnnotations(intent);
George Hodulik93562442019-03-21 17:35:23 -0700103 mPm = context.getPackageManager();
104 mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
105 mDefaultBrowserPackageName = mHttp
106 ? mPm.getDefaultBrowserPackageNameAsUser(UserHandle.myUserId())
107 : null;
Alison Cichowlas1fd47152019-11-14 19:50:55 -0500108 mAzComparator = new AzInfoComparator(context);
George Hodulik93562442019-03-21 17:35:23 -0700109 }
110
111 // get annotations of content from intent.
112 private void getContentAnnotations(Intent intent) {
113 ArrayList<String> annotations = intent.getStringArrayListExtra(
114 Intent.EXTRA_CONTENT_ANNOTATIONS);
115 if (annotations != null) {
116 int size = annotations.size();
117 if (size > NUM_OF_TOP_ANNOTATIONS_TO_USE) {
118 size = NUM_OF_TOP_ANNOTATIONS_TO_USE;
119 }
120 mAnnotations = new String[size];
121 for (int i = 0; i < size; i++) {
122 mAnnotations[i] = annotations.get(i);
123 }
124 }
125 }
George Hodulik30f0c6f2019-03-20 18:23:23 -0700126
127 /**
128 * Callback to be called when {@link #compute(List)} finishes. This signals to stop waiting.
129 */
George Hodulik93562442019-03-21 17:35:23 -0700130 interface AfterCompute {
George Hodulik30f0c6f2019-03-20 18:23:23 -0700131
George Hodulik93562442019-03-21 17:35:23 -0700132 void afterCompute();
George Hodulik30f0c6f2019-03-20 18:23:23 -0700133 }
134
George Hodulik93562442019-03-21 17:35:23 -0700135 void setCallBack(AfterCompute afterCompute) {
George Hodulik30f0c6f2019-03-20 18:23:23 -0700136 mAfterCompute = afterCompute;
137 }
138
George Hodulikc681ce42019-04-12 17:10:31 -0700139 protected final void afterCompute() {
140 final AfterCompute afterCompute = mAfterCompute;
141 if (afterCompute != null) {
142 afterCompute.afterCompute();
143 }
144 }
145
George Hodulik93562442019-03-21 17:35:23 -0700146 @Override
147 public final int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) {
148 final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
149 final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
150
151 // We want to put the one targeted to another user at the end of the dialog.
152 if (lhs.targetUserId != UserHandle.USER_CURRENT) {
153 return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1;
154 }
155 if (rhs.targetUserId != UserHandle.USER_CURRENT) {
156 return -1;
157 }
158
159 if (mHttp) {
160 // Special case: we want filters that match URI paths/schemes to be
161 // ordered before others. This is for the case when opening URIs,
162 // to make native apps go above browsers - except for 1 even more special case
163 // which is the default browser, as we want that to go above them all.
164 if (isDefaultBrowser(lhs)) {
165 return -1;
166 }
167
168 if (isDefaultBrowser(rhs)) {
169 return 1;
170 }
171 final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match);
172 final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match);
173 if (lhsSpecific != rhsSpecific) {
174 return lhsSpecific ? -1 : 1;
175 }
176 }
Alison Cichowlas1fd47152019-11-14 19:50:55 -0500177
178 final boolean lPinned = lhsp.isPinned();
179 final boolean rPinned = rhsp.isPinned();
180
181 // Pinned items always receive priority.
182 if (lPinned && !rPinned) {
183 return -1;
184 } else if (!lPinned && rPinned) {
185 return 1;
186 } else if (lPinned && rPinned) {
187 // If both items are pinned, resolve the tie alphabetically.
188 return mAzComparator.compare(lhsp.getResolveInfoAt(0), rhsp.getResolveInfoAt(0));
189 }
190
George Hodulik93562442019-03-21 17:35:23 -0700191 return compare(lhs, rhs);
192 }
193
194 /**
195 * Delegated to when used as a {@link Comparator<ResolvedComponentInfo>} if there is not a
196 * special case. The {@link ResolveInfo ResolveInfos} are the first {@link ResolveInfo} in
197 * {@link ResolvedComponentInfo#getResolveInfoAt(int)} from the parameters of {@link
198 * #compare(ResolvedComponentInfo, ResolvedComponentInfo)}
199 */
200 abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
201
George Hodulik30f0c6f2019-03-20 18:23:23 -0700202 /**
203 * Computes features for each target. This will be called before calls to {@link
204 * #getScore(ComponentName)} or {@link #compare(Object, Object)}, in order to prepare the
205 * comparator for those calls. Note that {@link #getScore(ComponentName)} uses {@link
206 * ComponentName}, so the implementation will have to be prepared to identify a {@link
George Hodulik1f5d9bf2019-04-29 14:55:48 -0700207 * ResolvedComponentInfo} by {@link ComponentName}. {@link #beforeCompute()} will be called
208 * before doing any computing.
George Hodulik30f0c6f2019-03-20 18:23:23 -0700209 */
George Hodulik1f5d9bf2019-04-29 14:55:48 -0700210 final void compute(List<ResolvedComponentInfo> targets) {
211 beforeCompute();
212 doCompute(targets);
213 }
214
215 /** Implementation of compute called after {@link #beforeCompute()}. */
216 abstract void doCompute(List<ResolvedComponentInfo> targets);
George Hodulik30f0c6f2019-03-20 18:23:23 -0700217
218 /**
219 * Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo}
220 * when {@link #compute(List)} was called before this.
221 */
George Hodulik93562442019-03-21 17:35:23 -0700222 abstract float getScore(ComponentName name);
George Hodulik30f0c6f2019-03-20 18:23:23 -0700223
George Hodulik1f5d9bf2019-04-29 14:55:48 -0700224 /** Handles result message sent to mHandler. */
225 abstract void handleResultMessage(Message message);
226
George Hodulik30f0c6f2019-03-20 18:23:23 -0700227 /**
228 * Reports to UsageStats what was chosen.
229 */
George Hodulik93562442019-03-21 17:35:23 -0700230 final void updateChooserCounts(String packageName, int userId, String action) {
231 if (mUsm != null) {
232 mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
233 }
234 }
George Hodulik30f0c6f2019-03-20 18:23:23 -0700235
236 /**
237 * Updates the model used to rank the componentNames.
238 *
239 * <p>Default implementation does nothing, as we could have simple model that does not train
240 * online.
241 *
242 * @param componentName the component that the user clicked
243 */
George Hodulik93562442019-03-21 17:35:23 -0700244 void updateModel(ComponentName componentName) {
George Hodulik30f0c6f2019-03-20 18:23:23 -0700245 }
246
George Hodulik1f5d9bf2019-04-29 14:55:48 -0700247 /** Called before {@link #doCompute(List)}. Sets up 500ms timeout. */
248 void beforeCompute() {
249 if (DEBUG) Log.d(TAG, "Setting watchdog timer for " + WATCHDOG_TIMEOUT_MILLIS + "ms");
250 if (mHandler == null) {
251 Log.d(TAG, "Error: Handler is Null; Needs to be initialized.");
252 return;
253 }
254 mHandler.sendEmptyMessageDelayed(RANKER_RESULT_TIMEOUT, WATCHDOG_TIMEOUT_MILLIS);
255 }
256
George Hodulik30f0c6f2019-03-20 18:23:23 -0700257 /**
George Hodulik1f5d9bf2019-04-29 14:55:48 -0700258 * Called when the {@link ResolverActivity} is destroyed. This calls {@link #afterCompute()}. If
259 * this call needs to happen at a different time during destroy, the method should be
260 * overridden.
George Hodulik30f0c6f2019-03-20 18:23:23 -0700261 */
George Hodulik1f5d9bf2019-04-29 14:55:48 -0700262 void destroy() {
263 mHandler.removeMessages(RANKER_SERVICE_RESULT);
264 mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
265 afterCompute();
266 }
George Hodulik93562442019-03-21 17:35:23 -0700267
268 private boolean isDefaultBrowser(ResolveInfo ri) {
269 // It makes sense to prefer the default browser
270 // only if the targeted user is the current user
271 if (ri.targetUserId != UserHandle.USER_CURRENT) {
272 return false;
273 }
274
275 if (ri.activityInfo.packageName != null
276 && ri.activityInfo.packageName.equals(mDefaultBrowserPackageName)) {
277 return true;
278 }
279 return false;
280 }
Alison Cichowlas1fd47152019-11-14 19:50:55 -0500281
282
283 /**
284 * Sort intents alphabetically based on package name.
285 */
286 class AzInfoComparator implements Comparator<ResolveInfo> {
287 Collator mCollator;
288 AzInfoComparator(Context context) {
289 mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
290 }
291
292 @Override
293 public int compare(ResolveInfo lhsp, ResolveInfo rhsp) {
294 if (lhsp == null) {
295 return -1;
296 } else if (rhsp == null) {
297 return 1;
298 }
299 return mCollator.compare(lhsp.activityInfo.packageName, rhsp.activityInfo.packageName);
300 }
301 }
302
George Hodulik30f0c6f2019-03-20 18:23:23 -0700303}