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