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