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