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