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