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