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