1 /* 2 * Copyright (C) 2015 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 18 package com.android.internal.app; 19 20 import android.app.usage.UsageStats; 21 import android.app.usage.UsageStatsManager; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.ComponentInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.pm.ResolveInfo; 31 import android.content.SharedPreferences; 32 import android.content.ServiceConnection; 33 import android.os.Environment; 34 import android.os.Handler; 35 import android.os.IBinder; 36 import android.os.Looper; 37 import android.os.Message; 38 import android.os.RemoteException; 39 import android.os.storage.StorageManager; 40 import android.os.UserHandle; 41 import android.service.resolver.IResolverRankerService; 42 import android.service.resolver.IResolverRankerResult; 43 import android.service.resolver.ResolverRankerService; 44 import android.service.resolver.ResolverTarget; 45 import android.text.TextUtils; 46 import android.util.ArrayMap; 47 import android.util.Log; 48 import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; 49 50 import java.io.File; 51 import java.lang.InterruptedException; 52 import java.text.Collator; 53 import java.util.ArrayList; 54 import java.util.Comparator; 55 import java.util.concurrent.CountDownLatch; 56 import java.util.concurrent.TimeUnit; 57 import java.util.LinkedHashMap; 58 import java.util.List; 59 import java.util.Map; 60 61 /** 62 * Ranks and compares packages based on usage stats. 63 */ 64 class ResolverComparator implements Comparator<ResolvedComponentInfo> { 65 private static final String TAG = "ResolverComparator"; 66 67 private static final boolean DEBUG = false; 68 69 private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3; 70 71 // One week 72 private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 7; 73 74 private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12; 75 76 private static final float RECENCY_MULTIPLIER = 2.f; 77 78 // message types 79 private static final int RESOLVER_RANKER_SERVICE_RESULT = 0; 80 private static final int RESOLVER_RANKER_RESULT_TIMEOUT = 1; 81 82 // timeout for establishing connections with a ResolverRankerService. 83 private static final int CONNECTION_COST_TIMEOUT_MILLIS = 200; 84 // timeout for establishing connections with a ResolverRankerService, collecting features and 85 // predicting ranking scores. 86 private static final int WATCHDOG_TIMEOUT_MILLIS = 500; 87 88 private final Collator mCollator; 89 private final boolean mHttp; 90 private final PackageManager mPm; 91 private final UsageStatsManager mUsm; 92 private final Map<String, UsageStats> mStats; 93 private final long mCurrentTime; 94 private final long mSinceTime; 95 private final LinkedHashMap<ComponentName, ResolverTarget> mTargetsDict = new LinkedHashMap<>(); 96 private final String mReferrerPackage; 97 private final Object mLock = new Object(); 98 private ArrayList<ResolverTarget> mTargets; 99 private String mContentType; 100 private String[] mAnnotations; 101 private String mAction; 102 private IResolverRankerService mRanker; 103 private ResolverRankerServiceConnection mConnection; 104 private AfterCompute mAfterCompute; 105 private Context mContext; 106 private CountDownLatch mConnectSignal; 107 108 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 109 public void handleMessage(Message msg) { 110 switch (msg.what) { 111 case RESOLVER_RANKER_SERVICE_RESULT: 112 if (DEBUG) { 113 Log.d(TAG, "RESOLVER_RANKER_SERVICE_RESULT"); 114 } 115 if (mHandler.hasMessages(RESOLVER_RANKER_RESULT_TIMEOUT)) { 116 if (msg.obj != null) { 117 final List<ResolverTarget> receivedTargets = 118 (List<ResolverTarget>) msg.obj; 119 if (receivedTargets != null && mTargets != null 120 && receivedTargets.size() == mTargets.size()) { 121 final int size = mTargets.size(); 122 for (int i = 0; i < size; ++i) { 123 mTargets.get(i).setSelectProbability( 124 receivedTargets.get(i).getSelectProbability()); 125 } 126 } else { 127 Log.e(TAG, "Sizes of sent and received ResolverTargets diff."); 128 } 129 } else { 130 Log.e(TAG, "Receiving null prediction results."); 131 } 132 mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT); 133 mAfterCompute.afterCompute(); 134 } 135 break; 136 137 case RESOLVER_RANKER_RESULT_TIMEOUT: 138 if (DEBUG) { 139 Log.d(TAG, "RESOLVER_RANKER_RESULT_TIMEOUT; unbinding services"); 140 } 141 mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT); 142 mAfterCompute.afterCompute(); 143 break; 144 145 default: 146 super.handleMessage(msg); 147 } 148 } 149 }; 150 151 public interface AfterCompute { afterCompute()152 public void afterCompute (); 153 } 154 ResolverComparator(Context context, Intent intent, String referrerPackage, AfterCompute afterCompute)155 public ResolverComparator(Context context, Intent intent, String referrerPackage, 156 AfterCompute afterCompute) { 157 mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); 158 String scheme = intent.getScheme(); 159 mHttp = "http".equals(scheme) || "https".equals(scheme); 160 mReferrerPackage = referrerPackage; 161 mAfterCompute = afterCompute; 162 mContext = context; 163 164 mPm = context.getPackageManager(); 165 mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); 166 167 mCurrentTime = System.currentTimeMillis(); 168 mSinceTime = mCurrentTime - USAGE_STATS_PERIOD; 169 mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime); 170 mContentType = intent.getType(); 171 getContentAnnotations(intent); 172 mAction = intent.getAction(); 173 } 174 175 // get annotations of content from intent. getContentAnnotations(Intent intent)176 public void getContentAnnotations(Intent intent) { 177 ArrayList<String> annotations = intent.getStringArrayListExtra( 178 Intent.EXTRA_CONTENT_ANNOTATIONS); 179 if (annotations != null) { 180 int size = annotations.size(); 181 if (size > NUM_OF_TOP_ANNOTATIONS_TO_USE) { 182 size = NUM_OF_TOP_ANNOTATIONS_TO_USE; 183 } 184 mAnnotations = new String[size]; 185 for (int i = 0; i < size; i++) { 186 mAnnotations[i] = annotations.get(i); 187 } 188 } 189 } 190 setCallBack(AfterCompute afterCompute)191 public void setCallBack(AfterCompute afterCompute) { 192 mAfterCompute = afterCompute; 193 } 194 195 // compute features for each target according to usage stats of targets. compute(List<ResolvedComponentInfo> targets)196 public void compute(List<ResolvedComponentInfo> targets) { 197 reset(); 198 199 final long recentSinceTime = mCurrentTime - RECENCY_TIME_PERIOD; 200 201 float mostRecencyScore = 1.0f; 202 float mostTimeSpentScore = 1.0f; 203 float mostLaunchScore = 1.0f; 204 float mostChooserScore = 1.0f; 205 206 for (ResolvedComponentInfo target : targets) { 207 final ResolverTarget resolverTarget = new ResolverTarget(); 208 mTargetsDict.put(target.name, resolverTarget); 209 final UsageStats pkStats = mStats.get(target.name.getPackageName()); 210 if (pkStats != null) { 211 // Only count recency for apps that weren't the caller 212 // since the caller is always the most recent. 213 // Persistent processes muck this up, so omit them too. 214 if (!target.name.getPackageName().equals(mReferrerPackage) 215 && !isPersistentProcess(target)) { 216 final float recencyScore = 217 (float) Math.max(pkStats.getLastTimeUsed() - recentSinceTime, 0); 218 resolverTarget.setRecencyScore(recencyScore); 219 if (recencyScore > mostRecencyScore) { 220 mostRecencyScore = recencyScore; 221 } 222 } 223 final float timeSpentScore = (float) pkStats.getTotalTimeInForeground(); 224 resolverTarget.setTimeSpentScore(timeSpentScore); 225 if (timeSpentScore > mostTimeSpentScore) { 226 mostTimeSpentScore = timeSpentScore; 227 } 228 final float launchScore = (float) pkStats.mLaunchCount; 229 resolverTarget.setLaunchScore(launchScore); 230 if (launchScore > mostLaunchScore) { 231 mostLaunchScore = launchScore; 232 } 233 234 float chooserScore = 0.0f; 235 if (pkStats.mChooserCounts != null && mAction != null 236 && pkStats.mChooserCounts.get(mAction) != null) { 237 chooserScore = (float) pkStats.mChooserCounts.get(mAction) 238 .getOrDefault(mContentType, 0); 239 if (mAnnotations != null) { 240 final int size = mAnnotations.length; 241 for (int i = 0; i < size; i++) { 242 chooserScore += (float) pkStats.mChooserCounts.get(mAction) 243 .getOrDefault(mAnnotations[i], 0); 244 } 245 } 246 } 247 if (DEBUG) { 248 if (mAction == null) { 249 Log.d(TAG, "Action type is null"); 250 } else { 251 Log.d(TAG, "Chooser Count of " + mAction + ":" + 252 target.name.getPackageName() + " is " + 253 Float.toString(chooserScore)); 254 } 255 } 256 resolverTarget.setChooserScore(chooserScore); 257 if (chooserScore > mostChooserScore) { 258 mostChooserScore = chooserScore; 259 } 260 } 261 } 262 263 if (DEBUG) { 264 Log.d(TAG, "compute - mostRecencyScore: " + mostRecencyScore 265 + " mostTimeSpentScore: " + mostTimeSpentScore 266 + " mostLaunchScore: " + mostLaunchScore 267 + " mostChooserScore: " + mostChooserScore); 268 } 269 270 mTargets = new ArrayList<>(mTargetsDict.values()); 271 for (ResolverTarget target : mTargets) { 272 final float recency = target.getRecencyScore() / mostRecencyScore; 273 setFeatures(target, recency * recency * RECENCY_MULTIPLIER, 274 target.getLaunchScore() / mostLaunchScore, 275 target.getTimeSpentScore() / mostTimeSpentScore, 276 target.getChooserScore() / mostChooserScore); 277 addDefaultSelectProbability(target); 278 if (DEBUG) { 279 Log.d(TAG, "Scores: " + target); 280 } 281 } 282 predictSelectProbabilities(mTargets); 283 } 284 285 @Override compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp)286 public int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) { 287 final ResolveInfo lhs = lhsp.getResolveInfoAt(0); 288 final ResolveInfo rhs = rhsp.getResolveInfoAt(0); 289 290 // We want to put the one targeted to another user at the end of the dialog. 291 if (lhs.targetUserId != UserHandle.USER_CURRENT) { 292 return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1; 293 } 294 if (rhs.targetUserId != UserHandle.USER_CURRENT) { 295 return -1; 296 } 297 298 if (mHttp) { 299 // Special case: we want filters that match URI paths/schemes to be 300 // ordered before others. This is for the case when opening URIs, 301 // to make native apps go above browsers. 302 final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match); 303 final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match); 304 if (lhsSpecific != rhsSpecific) { 305 return lhsSpecific ? -1 : 1; 306 } 307 } 308 309 final boolean lPinned = lhsp.isPinned(); 310 final boolean rPinned = rhsp.isPinned(); 311 312 if (lPinned && !rPinned) { 313 return -1; 314 } else if (!lPinned && rPinned) { 315 return 1; 316 } 317 318 // Pinned items stay stable within a normal lexical sort and ignore scoring. 319 if (!lPinned && !rPinned) { 320 if (mStats != null) { 321 final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName( 322 lhs.activityInfo.packageName, lhs.activityInfo.name)); 323 final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName( 324 rhs.activityInfo.packageName, rhs.activityInfo.name)); 325 326 final int selectProbabilityDiff = Float.compare( 327 rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability()); 328 329 if (selectProbabilityDiff != 0) { 330 return selectProbabilityDiff > 0 ? 1 : -1; 331 } 332 } 333 } 334 335 CharSequence sa = lhs.loadLabel(mPm); 336 if (sa == null) sa = lhs.activityInfo.name; 337 CharSequence sb = rhs.loadLabel(mPm); 338 if (sb == null) sb = rhs.activityInfo.name; 339 340 return mCollator.compare(sa.toString().trim(), sb.toString().trim()); 341 } 342 getScore(ComponentName name)343 public float getScore(ComponentName name) { 344 final ResolverTarget target = mTargetsDict.get(name); 345 if (target != null) { 346 return target.getSelectProbability(); 347 } 348 return 0; 349 } 350 updateChooserCounts(String packageName, int userId, String action)351 public void updateChooserCounts(String packageName, int userId, String action) { 352 if (mUsm != null) { 353 mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action); 354 } 355 } 356 357 // update ranking model when the connection to it is valid. updateModel(ComponentName componentName)358 public void updateModel(ComponentName componentName) { 359 synchronized (mLock) { 360 if (mRanker != null) { 361 try { 362 int selectedPos = new ArrayList<ComponentName>(mTargetsDict.keySet()) 363 .indexOf(componentName); 364 if (selectedPos > 0) { 365 mRanker.train(mTargets, selectedPos); 366 } else { 367 if (DEBUG) { 368 Log.d(TAG, "Selected a unknown component: " + componentName); 369 } 370 } 371 } catch (RemoteException e) { 372 Log.e(TAG, "Error in Train: " + e); 373 } 374 } else { 375 if (DEBUG) { 376 Log.d(TAG, "Ranker is null; skip updateModel."); 377 } 378 } 379 } 380 } 381 382 // unbind the service and clear unhandled messges. destroy()383 public void destroy() { 384 mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT); 385 mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT); 386 if (mConnection != null) { 387 mContext.unbindService(mConnection); 388 mConnection.destroy(); 389 } 390 if (DEBUG) { 391 Log.d(TAG, "Unbinded Resolver Ranker."); 392 } 393 } 394 395 // connect to a ranking service. initRanker(Context context)396 private void initRanker(Context context) { 397 synchronized (mLock) { 398 if (mConnection != null && mRanker != null) { 399 if (DEBUG) { 400 Log.d(TAG, "Ranker still exists; reusing the existing one."); 401 } 402 return; 403 } 404 } 405 Intent intent = resolveRankerService(); 406 if (intent == null) { 407 return; 408 } 409 mConnectSignal = new CountDownLatch(1); 410 mConnection = new ResolverRankerServiceConnection(mConnectSignal); 411 context.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM); 412 } 413 414 // resolve the service for ranking. resolveRankerService()415 private Intent resolveRankerService() { 416 Intent intent = new Intent(ResolverRankerService.SERVICE_INTERFACE); 417 final List<ResolveInfo> resolveInfos = mPm.queryIntentServices(intent, 0); 418 for (ResolveInfo resolveInfo : resolveInfos) { 419 if (resolveInfo == null || resolveInfo.serviceInfo == null 420 || resolveInfo.serviceInfo.applicationInfo == null) { 421 if (DEBUG) { 422 Log.d(TAG, "Failed to retrieve a ranker: " + resolveInfo); 423 } 424 continue; 425 } 426 ComponentName componentName = new ComponentName( 427 resolveInfo.serviceInfo.applicationInfo.packageName, 428 resolveInfo.serviceInfo.name); 429 try { 430 final String perm = mPm.getServiceInfo(componentName, 0).permission; 431 if (!ResolverRankerService.BIND_PERMISSION.equals(perm)) { 432 Log.w(TAG, "ResolverRankerService " + componentName + " does not require" 433 + " permission " + ResolverRankerService.BIND_PERMISSION 434 + " - this service will not be queried for ResolverComparator." 435 + " add android:permission=\"" 436 + ResolverRankerService.BIND_PERMISSION + "\"" 437 + " to the <service> tag for " + componentName 438 + " in the manifest."); 439 continue; 440 } 441 if (PackageManager.PERMISSION_GRANTED != mPm.checkPermission( 442 ResolverRankerService.HOLD_PERMISSION, 443 resolveInfo.serviceInfo.packageName)) { 444 Log.w(TAG, "ResolverRankerService " + componentName + " does not hold" 445 + " permission " + ResolverRankerService.HOLD_PERMISSION 446 + " - this service will not be queried for ResolverComparator."); 447 continue; 448 } 449 } catch (NameNotFoundException e) { 450 Log.e(TAG, "Could not look up service " + componentName 451 + "; component name not found"); 452 continue; 453 } 454 if (DEBUG) { 455 Log.d(TAG, "Succeeded to retrieve a ranker: " + componentName); 456 } 457 intent.setComponent(componentName); 458 return intent; 459 } 460 return null; 461 } 462 463 // set a watchdog, to avoid waiting for ranking service for too long. startWatchDog(int timeOutLimit)464 private void startWatchDog(int timeOutLimit) { 465 if (DEBUG) Log.d(TAG, "Setting watchdog timer for " + timeOutLimit + "ms"); 466 if (mHandler == null) { 467 Log.d(TAG, "Error: Handler is Null; Needs to be initialized."); 468 } 469 mHandler.sendEmptyMessageDelayed(RESOLVER_RANKER_RESULT_TIMEOUT, timeOutLimit); 470 } 471 472 private class ResolverRankerServiceConnection implements ServiceConnection { 473 private final CountDownLatch mConnectSignal; 474 ResolverRankerServiceConnection(CountDownLatch connectSignal)475 public ResolverRankerServiceConnection(CountDownLatch connectSignal) { 476 mConnectSignal = connectSignal; 477 } 478 479 public final IResolverRankerResult resolverRankerResult = 480 new IResolverRankerResult.Stub() { 481 @Override 482 public void sendResult(List<ResolverTarget> targets) throws RemoteException { 483 if (DEBUG) { 484 Log.d(TAG, "Sending Result back to Resolver: " + targets); 485 } 486 synchronized (mLock) { 487 final Message msg = Message.obtain(); 488 msg.what = RESOLVER_RANKER_SERVICE_RESULT; 489 msg.obj = targets; 490 mHandler.sendMessage(msg); 491 } 492 } 493 }; 494 495 @Override onServiceConnected(ComponentName name, IBinder service)496 public void onServiceConnected(ComponentName name, IBinder service) { 497 if (DEBUG) { 498 Log.d(TAG, "onServiceConnected: " + name); 499 } 500 synchronized (mLock) { 501 mRanker = IResolverRankerService.Stub.asInterface(service); 502 mConnectSignal.countDown(); 503 } 504 } 505 506 @Override onServiceDisconnected(ComponentName name)507 public void onServiceDisconnected(ComponentName name) { 508 if (DEBUG) { 509 Log.d(TAG, "onServiceDisconnected: " + name); 510 } 511 synchronized (mLock) { 512 destroy(); 513 } 514 } 515 destroy()516 public void destroy() { 517 synchronized (mLock) { 518 mRanker = null; 519 } 520 } 521 } 522 reset()523 private void reset() { 524 mTargetsDict.clear(); 525 mTargets = null; 526 startWatchDog(WATCHDOG_TIMEOUT_MILLIS); 527 initRanker(mContext); 528 } 529 530 // predict select probabilities if ranking service is valid. predictSelectProbabilities(List<ResolverTarget> targets)531 private void predictSelectProbabilities(List<ResolverTarget> targets) { 532 if (mConnection == null) { 533 if (DEBUG) { 534 Log.d(TAG, "Has not found valid ResolverRankerService; Skip Prediction"); 535 } 536 return; 537 } else { 538 try { 539 mConnectSignal.await(CONNECTION_COST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 540 synchronized (mLock) { 541 if (mRanker != null) { 542 mRanker.predict(targets, mConnection.resolverRankerResult); 543 return; 544 } else { 545 if (DEBUG) { 546 Log.d(TAG, "Ranker has not been initialized; skip predict."); 547 } 548 } 549 } 550 } catch (InterruptedException e) { 551 Log.e(TAG, "Error in Wait for Service Connection."); 552 } catch (RemoteException e) { 553 Log.e(TAG, "Error in Predict: " + e); 554 } 555 } 556 mAfterCompute.afterCompute(); 557 } 558 559 // adds select prob as the default values, according to a pre-trained Logistic Regression model. addDefaultSelectProbability(ResolverTarget target)560 private void addDefaultSelectProbability(ResolverTarget target) { 561 float sum = 2.5543f * target.getLaunchScore() + 2.8412f * target.getTimeSpentScore() + 562 0.269f * target.getRecencyScore() + 4.2222f * target.getChooserScore(); 563 target.setSelectProbability((float) (1.0 / (1.0 + Math.exp(1.6568f - sum)))); 564 } 565 566 // sets features for each target setFeatures(ResolverTarget target, float recencyScore, float launchScore, float timeSpentScore, float chooserScore)567 private void setFeatures(ResolverTarget target, float recencyScore, float launchScore, 568 float timeSpentScore, float chooserScore) { 569 target.setRecencyScore(recencyScore); 570 target.setLaunchScore(launchScore); 571 target.setTimeSpentScore(timeSpentScore); 572 target.setChooserScore(chooserScore); 573 } 574 isPersistentProcess(ResolvedComponentInfo rci)575 static boolean isPersistentProcess(ResolvedComponentInfo rci) { 576 if (rci != null && rci.getCount() > 0) { 577 return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags & 578 ApplicationInfo.FLAG_PERSISTENT) != 0; 579 } 580 return false; 581 } 582 } 583