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