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