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