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