1 /* 2 * Copyright (C) 2011 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 android.widget; 18 19 import android.annotation.Nullable; 20 import android.app.ActivityManager; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.database.DataSetObservable; 29 import android.os.AsyncTask; 30 import android.os.Build; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.util.Xml; 34 35 import com.android.internal.content.PackageMonitor; 36 37 import org.xmlpull.v1.XmlPullParser; 38 import org.xmlpull.v1.XmlPullParserException; 39 import org.xmlpull.v1.XmlSerializer; 40 41 import java.io.FileInputStream; 42 import java.io.FileNotFoundException; 43 import java.io.FileOutputStream; 44 import java.io.IOException; 45 import java.math.BigDecimal; 46 import java.nio.charset.StandardCharsets; 47 import java.util.ArrayList; 48 import java.util.Collections; 49 import java.util.HashMap; 50 import java.util.List; 51 import java.util.Map; 52 53 /** 54 * <p> 55 * This class represents a data model for choosing a component for handing a 56 * given {@link Intent}. The model is responsible for querying the system for 57 * activities that can handle the given intent and order found activities 58 * based on historical data of previous choices. The historical data is stored 59 * in an application private file. If a client does not want to have persistent 60 * choice history the file can be omitted, thus the activities will be ordered 61 * based on historical usage for the current session. 62 * <p> 63 * </p> 64 * For each backing history file there is a singleton instance of this class. Thus, 65 * several clients that specify the same history file will share the same model. Note 66 * that if multiple clients are sharing the same model they should implement semantically 67 * equivalent functionality since setting the model intent will change the found 68 * activities and they may be inconsistent with the functionality of some of the clients. 69 * For example, choosing a share activity can be implemented by a single backing 70 * model and two different views for performing the selection. If however, one of the 71 * views is used for sharing but the other for importing, for example, then each 72 * view should be backed by a separate model. 73 * </p> 74 * <p> 75 * The way clients interact with this class is as follows: 76 * </p> 77 * <p> 78 * <pre> 79 * <code> 80 * // Get a model and set it to a couple of clients with semantically similar function. 81 * ActivityChooserModel dataModel = 82 * ActivityChooserModel.get(context, "task_specific_history_file_name.xml"); 83 * 84 * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1(); 85 * modelClient1.setActivityChooserModel(dataModel); 86 * 87 * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2(); 88 * modelClient2.setActivityChooserModel(dataModel); 89 * 90 * // Set an intent to choose a an activity for. 91 * dataModel.setIntent(intent); 92 * <pre> 93 * <code> 94 * </p> 95 * <p> 96 * <strong>Note:</strong> This class is thread safe. 97 * </p> 98 * 99 * @hide 100 */ 101 public class ActivityChooserModel extends DataSetObservable { 102 103 /** 104 * Client that utilizes an {@link ActivityChooserModel}. 105 */ 106 public interface ActivityChooserModelClient { 107 108 /** 109 * Sets the {@link ActivityChooserModel}. 110 * 111 * @param dataModel The model. 112 */ setActivityChooserModel(ActivityChooserModel dataModel)113 public void setActivityChooserModel(ActivityChooserModel dataModel); 114 } 115 116 /** 117 * Defines a sorter that is responsible for sorting the activities 118 * based on the provided historical choices and an intent. 119 */ 120 public interface ActivitySorter { 121 122 /** 123 * Sorts the <code>activities</code> in descending order of relevance 124 * based on previous history and an intent. 125 * 126 * @param intent The {@link Intent}. 127 * @param activities Activities to be sorted. 128 * @param historicalRecords Historical records. 129 */ 130 // This cannot be done by a simple comparator since an Activity weight 131 // is computed from history. Note that Activity implements Comparable. sort(Intent intent, List<ActivityResolveInfo> activities, List<HistoricalRecord> historicalRecords)132 public void sort(Intent intent, List<ActivityResolveInfo> activities, 133 List<HistoricalRecord> historicalRecords); 134 } 135 136 /** 137 * Listener for choosing an activity. 138 */ 139 public interface OnChooseActivityListener { 140 141 /** 142 * Called when an activity has been chosen. The client can decide whether 143 * an activity can be chosen and if so the caller of 144 * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent} 145 * for launching it. 146 * <p> 147 * <strong>Note:</strong> Modifying the intent is not permitted and 148 * any changes to the latter will be ignored. 149 * </p> 150 * 151 * @param host The listener's host model. 152 * @param intent The intent for launching the chosen activity. 153 * @return Whether the intent is handled and should not be delivered to clients. 154 * 155 * @see ActivityChooserModel#chooseActivity(int) 156 */ onChooseActivity(ActivityChooserModel host, Intent intent)157 public boolean onChooseActivity(ActivityChooserModel host, Intent intent); 158 } 159 160 /** 161 * Flag for selecting debug mode. 162 */ 163 private static final boolean DEBUG = false; 164 165 /** 166 * Tag used for logging. 167 */ 168 private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName(); 169 170 /** 171 * The root tag in the history file. 172 */ 173 private static final String TAG_HISTORICAL_RECORDS = "historical-records"; 174 175 /** 176 * The tag for a record in the history file. 177 */ 178 private static final String TAG_HISTORICAL_RECORD = "historical-record"; 179 180 /** 181 * Attribute for the activity. 182 */ 183 private static final String ATTRIBUTE_ACTIVITY = "activity"; 184 185 /** 186 * Attribute for the choice time. 187 */ 188 private static final String ATTRIBUTE_TIME = "time"; 189 190 /** 191 * Attribute for the choice weight. 192 */ 193 private static final String ATTRIBUTE_WEIGHT = "weight"; 194 195 /** 196 * The default name of the choice history file. 197 */ 198 public static final String DEFAULT_HISTORY_FILE_NAME = 199 "activity_choser_model_history.xml"; 200 201 /** 202 * The default maximal length of the choice history. 203 */ 204 public static final int DEFAULT_HISTORY_MAX_LENGTH = 50; 205 206 /** 207 * The amount with which to inflate a chosen activity when set as default. 208 */ 209 private static final int DEFAULT_ACTIVITY_INFLATION = 5; 210 211 /** 212 * Default weight for a choice record. 213 */ 214 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f; 215 216 /** 217 * The extension of the history file. 218 */ 219 private static final String HISTORY_FILE_EXTENSION = ".xml"; 220 221 /** 222 * An invalid item index. 223 */ 224 private static final int INVALID_INDEX = -1; 225 226 /** 227 * Lock to guard the model registry. 228 */ 229 private static final Object sRegistryLock = new Object(); 230 231 /** 232 * This the registry for data models. 233 */ 234 private static final Map<String, ActivityChooserModel> sDataModelRegistry = 235 new HashMap<String, ActivityChooserModel>(); 236 237 /** 238 * Lock for synchronizing on this instance. 239 */ 240 private final Object mInstanceLock = new Object(); 241 242 /** 243 * List of activities that can handle the current intent. 244 */ 245 private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>(); 246 247 /** 248 * List with historical choice records. 249 */ 250 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>(); 251 252 /** 253 * Monitor for added and removed packages. 254 */ 255 private final PackageMonitor mPackageMonitor = new DataModelPackageMonitor(); 256 257 /** 258 * Context for accessing resources. 259 */ 260 private final Context mContext; 261 262 /** 263 * The name of the history file that backs this model. 264 */ 265 private final String mHistoryFileName; 266 267 /** 268 * The intent for which a activity is being chosen. 269 */ 270 private Intent mIntent; 271 272 /** 273 * The sorter for ordering activities based on intent and past choices. 274 */ 275 private ActivitySorter mActivitySorter = new DefaultSorter(); 276 277 /** 278 * The maximal length of the choice history. 279 */ 280 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH; 281 282 /** 283 * Flag whether choice history can be read. In general many clients can 284 * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called 285 * by arbitrary of them any number of times. Therefore, this class guarantees 286 * that the very first read succeeds and subsequent reads can be performed 287 * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change 288 * of the share records. 289 */ 290 private boolean mCanReadHistoricalData = true; 291 292 /** 293 * Flag whether the choice history was read. This is used to enforce that 294 * before calling {@link #persistHistoricalDataIfNeeded()} a call to 295 * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a 296 * scenario in which a choice history file exits, it is not read yet and 297 * it is overwritten. Note that always all historical records are read in 298 * full and the file is rewritten. This is necessary since we need to 299 * purge old records that are outside of the sliding window of past choices. 300 */ 301 private boolean mReadShareHistoryCalled = false; 302 303 /** 304 * Flag whether the choice records have changed. In general many clients can 305 * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called 306 * by arbitrary of them any number of times. Therefore, this class guarantees 307 * that choice history will be persisted only if it has changed. 308 */ 309 private boolean mHistoricalRecordsChanged = true; 310 311 /** 312 * Flag whether to reload the activities for the current intent. 313 */ 314 private boolean mReloadActivities = false; 315 316 /** 317 * Policy for controlling how the model handles chosen activities. 318 */ 319 private OnChooseActivityListener mActivityChoserModelPolicy; 320 321 /** 322 * Gets the data model backed by the contents of the provided file with historical data. 323 * Note that only one data model is backed by a given file, thus multiple calls with 324 * the same file name will return the same model instance. If no such instance is present 325 * it is created. 326 * <p> 327 * <strong>Note:</strong> To use the default historical data file clients should explicitly 328 * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice 329 * history is desired clients should pass <code>null</code> for the file name. In such 330 * case a new model is returned for each invocation. 331 * </p> 332 * 333 * <p> 334 * <strong>Always use difference historical data files for semantically different actions. 335 * For example, sharing is different from importing.</strong> 336 * </p> 337 * 338 * @param context Context for loading resources. 339 * @param historyFileName File name with choice history, <code>null</code> 340 * if the model should not be backed by a file. In this case the activities 341 * will be ordered only by data from the current session. 342 * 343 * @return The model. 344 */ 345 @UnsupportedAppUsage get(Context context, String historyFileName)346 public static ActivityChooserModel get(Context context, String historyFileName) { 347 synchronized (sRegistryLock) { 348 ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName); 349 if (dataModel == null) { 350 dataModel = new ActivityChooserModel(context, historyFileName); 351 sDataModelRegistry.put(historyFileName, dataModel); 352 } 353 return dataModel; 354 } 355 } 356 357 /** 358 * Creates a new instance. 359 * 360 * @param context Context for loading resources. 361 * @param historyFileName The history XML file. 362 */ ActivityChooserModel(Context context, String historyFileName)363 private ActivityChooserModel(Context context, String historyFileName) { 364 mContext = context.getApplicationContext(); 365 if (!TextUtils.isEmpty(historyFileName) 366 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) { 367 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION; 368 } else { 369 mHistoryFileName = historyFileName; 370 } 371 mPackageMonitor.register(mContext, null, true); 372 } 373 374 /** 375 * Sets an intent for which to choose a activity. 376 * <p> 377 * <strong>Note:</strong> Clients must set only semantically similar 378 * intents for each data model. 379 * <p> 380 * 381 * @param intent The intent. 382 */ 383 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) setIntent(Intent intent)384 public void setIntent(Intent intent) { 385 synchronized (mInstanceLock) { 386 if (mIntent == intent) { 387 return; 388 } 389 mIntent = intent; 390 mReloadActivities = true; 391 ensureConsistentState(); 392 } 393 } 394 395 /** 396 * Gets the intent for which a activity is being chosen. 397 * 398 * @return The intent. 399 */ getIntent()400 public Intent getIntent() { 401 synchronized (mInstanceLock) { 402 return mIntent; 403 } 404 } 405 406 /** 407 * Gets the number of activities that can handle the intent. 408 * 409 * @return The activity count. 410 * 411 * @see #setIntent(Intent) 412 */ 413 @UnsupportedAppUsage getActivityCount()414 public int getActivityCount() { 415 synchronized (mInstanceLock) { 416 ensureConsistentState(); 417 return mActivities.size(); 418 } 419 } 420 421 /** 422 * Gets an activity at a given index. 423 * 424 * @return The activity. 425 * 426 * @see ActivityResolveInfo 427 * @see #setIntent(Intent) 428 */ 429 @UnsupportedAppUsage getActivity(int index)430 public ResolveInfo getActivity(int index) { 431 synchronized (mInstanceLock) { 432 ensureConsistentState(); 433 return mActivities.get(index).resolveInfo; 434 } 435 } 436 437 /** 438 * Gets the index of a the given activity. 439 * 440 * @param activity The activity index. 441 * 442 * @return The index if found, -1 otherwise. 443 */ getActivityIndex(ResolveInfo activity)444 public int getActivityIndex(ResolveInfo activity) { 445 synchronized (mInstanceLock) { 446 ensureConsistentState(); 447 List<ActivityResolveInfo> activities = mActivities; 448 final int activityCount = activities.size(); 449 for (int i = 0; i < activityCount; i++) { 450 ActivityResolveInfo currentActivity = activities.get(i); 451 if (currentActivity.resolveInfo == activity) { 452 return i; 453 } 454 } 455 return INVALID_INDEX; 456 } 457 } 458 459 /** 460 * Chooses a activity to handle the current intent. This will result in 461 * adding a historical record for that action and construct intent with 462 * its component name set such that it can be immediately started by the 463 * client. 464 * <p> 465 * <strong>Note:</strong> By calling this method the client guarantees 466 * that the returned intent will be started. This intent is returned to 467 * the client solely to let additional customization before the start. 468 * </p> 469 * 470 * @return An {@link Intent} for launching the activity or null if the 471 * policy has consumed the intent or there is not current intent 472 * set via {@link #setIntent(Intent)}. 473 * 474 * @see HistoricalRecord 475 * @see OnChooseActivityListener 476 */ 477 @UnsupportedAppUsage chooseActivity(int index)478 public Intent chooseActivity(int index) { 479 synchronized (mInstanceLock) { 480 if (mIntent == null) { 481 return null; 482 } 483 484 ensureConsistentState(); 485 486 ActivityResolveInfo chosenActivity = mActivities.get(index); 487 488 ComponentName chosenName = new ComponentName( 489 chosenActivity.resolveInfo.activityInfo.packageName, 490 chosenActivity.resolveInfo.activityInfo.name); 491 492 Intent choiceIntent = new Intent(mIntent); 493 choiceIntent.setComponent(chosenName); 494 495 if (mActivityChoserModelPolicy != null) { 496 // Do not allow the policy to change the intent. 497 Intent choiceIntentCopy = new Intent(choiceIntent); 498 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this, 499 choiceIntentCopy); 500 if (handled) { 501 return null; 502 } 503 } 504 505 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName, 506 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT); 507 addHisoricalRecord(historicalRecord); 508 509 return choiceIntent; 510 } 511 } 512 513 /** 514 * Sets the listener for choosing an activity. 515 * 516 * @param listener The listener. 517 */ 518 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) setOnChooseActivityListener(OnChooseActivityListener listener)519 public void setOnChooseActivityListener(OnChooseActivityListener listener) { 520 synchronized (mInstanceLock) { 521 mActivityChoserModelPolicy = listener; 522 } 523 } 524 525 /** 526 * Gets the default activity, The default activity is defined as the one 527 * with highest rank i.e. the first one in the list of activities that can 528 * handle the intent. 529 * 530 * @return The default activity, <code>null</code> id not activities. 531 * 532 * @see #getActivity(int) 533 */ getDefaultActivity()534 public ResolveInfo getDefaultActivity() { 535 synchronized (mInstanceLock) { 536 ensureConsistentState(); 537 if (!mActivities.isEmpty()) { 538 return mActivities.get(0).resolveInfo; 539 } 540 } 541 return null; 542 } 543 544 /** 545 * Sets the default activity. The default activity is set by adding a 546 * historical record with weight high enough that this activity will 547 * become the highest ranked. Such a strategy guarantees that the default 548 * will eventually change if not used. Also the weight of the record for 549 * setting a default is inflated with a constant amount to guarantee that 550 * it will stay as default for awhile. 551 * 552 * @param index The index of the activity to set as default. 553 */ setDefaultActivity(int index)554 public void setDefaultActivity(int index) { 555 synchronized (mInstanceLock) { 556 ensureConsistentState(); 557 558 ActivityResolveInfo newDefaultActivity = mActivities.get(index); 559 ActivityResolveInfo oldDefaultActivity = mActivities.get(0); 560 561 final float weight; 562 if (oldDefaultActivity != null) { 563 // Add a record with weight enough to boost the chosen at the top. 564 weight = oldDefaultActivity.weight - newDefaultActivity.weight 565 + DEFAULT_ACTIVITY_INFLATION; 566 } else { 567 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT; 568 } 569 570 ComponentName defaultName = new ComponentName( 571 newDefaultActivity.resolveInfo.activityInfo.packageName, 572 newDefaultActivity.resolveInfo.activityInfo.name); 573 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName, 574 System.currentTimeMillis(), weight); 575 addHisoricalRecord(historicalRecord); 576 } 577 } 578 579 /** 580 * Persists the history data to the backing file if the latter 581 * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()} 582 * throws an exception. Calling this method more than one without choosing an 583 * activity has not effect. 584 * 585 * @throws IllegalStateException If this method is called before a call to 586 * {@link #readHistoricalDataIfNeeded()}. 587 */ persistHistoricalDataIfNeeded()588 private void persistHistoricalDataIfNeeded() { 589 if (!mReadShareHistoryCalled) { 590 throw new IllegalStateException("No preceding call to #readHistoricalData"); 591 } 592 if (!mHistoricalRecordsChanged) { 593 return; 594 } 595 mHistoricalRecordsChanged = false; 596 if (!TextUtils.isEmpty(mHistoryFileName)) { 597 new PersistHistoryAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, 598 new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName); 599 } 600 } 601 602 /** 603 * Sets the sorter for ordering activities based on historical data and an intent. 604 * 605 * @param activitySorter The sorter. 606 * 607 * @see ActivitySorter 608 */ setActivitySorter(ActivitySorter activitySorter)609 public void setActivitySorter(ActivitySorter activitySorter) { 610 synchronized (mInstanceLock) { 611 if (mActivitySorter == activitySorter) { 612 return; 613 } 614 mActivitySorter = activitySorter; 615 if (sortActivitiesIfNeeded()) { 616 notifyChanged(); 617 } 618 } 619 } 620 621 /** 622 * Sets the maximal size of the historical data. Defaults to 623 * {@link #DEFAULT_HISTORY_MAX_LENGTH} 624 * <p> 625 * <strong>Note:</strong> Setting this property will immediately 626 * enforce the specified max history size by dropping enough old 627 * historical records to enforce the desired size. Thus, any 628 * records that exceed the history size will be discarded and 629 * irreversibly lost. 630 * </p> 631 * 632 * @param historyMaxSize The max history size. 633 */ setHistoryMaxSize(int historyMaxSize)634 public void setHistoryMaxSize(int historyMaxSize) { 635 synchronized (mInstanceLock) { 636 if (mHistoryMaxSize == historyMaxSize) { 637 return; 638 } 639 mHistoryMaxSize = historyMaxSize; 640 pruneExcessiveHistoricalRecordsIfNeeded(); 641 if (sortActivitiesIfNeeded()) { 642 notifyChanged(); 643 } 644 } 645 } 646 647 /** 648 * Gets the history max size. 649 * 650 * @return The history max size. 651 */ getHistoryMaxSize()652 public int getHistoryMaxSize() { 653 synchronized (mInstanceLock) { 654 return mHistoryMaxSize; 655 } 656 } 657 658 /** 659 * Gets the history size. 660 * 661 * @return The history size. 662 */ getHistorySize()663 public int getHistorySize() { 664 synchronized (mInstanceLock) { 665 ensureConsistentState(); 666 return mHistoricalRecords.size(); 667 } 668 } 669 670 @Override finalize()671 protected void finalize() throws Throwable { 672 super.finalize(); 673 mPackageMonitor.unregister(); 674 } 675 676 /** 677 * Ensures the model is in a consistent state which is the 678 * activities for the current intent have been loaded, the 679 * most recent history has been read, and the activities 680 * are sorted. 681 */ ensureConsistentState()682 private void ensureConsistentState() { 683 boolean stateChanged = loadActivitiesIfNeeded(); 684 stateChanged |= readHistoricalDataIfNeeded(); 685 pruneExcessiveHistoricalRecordsIfNeeded(); 686 if (stateChanged) { 687 sortActivitiesIfNeeded(); 688 notifyChanged(); 689 } 690 } 691 692 /** 693 * Sorts the activities if necessary which is if there is a 694 * sorter, there are some activities to sort, and there is some 695 * historical data. 696 * 697 * @return Whether sorting was performed. 698 */ sortActivitiesIfNeeded()699 private boolean sortActivitiesIfNeeded() { 700 if (mActivitySorter != null && mIntent != null 701 && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) { 702 mActivitySorter.sort(mIntent, mActivities, 703 Collections.unmodifiableList(mHistoricalRecords)); 704 return true; 705 } 706 return false; 707 } 708 709 /** 710 * Loads the activities for the current intent if needed which is 711 * if they are not already loaded for the current intent. 712 * 713 * @return Whether loading was performed. 714 */ loadActivitiesIfNeeded()715 private boolean loadActivitiesIfNeeded() { 716 if (mReloadActivities && mIntent != null) { 717 mReloadActivities = false; 718 mActivities.clear(); 719 List<ResolveInfo> resolveInfos = mContext.getPackageManager() 720 .queryIntentActivities(mIntent, 0); 721 final int resolveInfoCount = resolveInfos.size(); 722 for (int i = 0; i < resolveInfoCount; i++) { 723 ResolveInfo resolveInfo = resolveInfos.get(i); 724 ActivityInfo activityInfo = resolveInfo.activityInfo; 725 if (ActivityManager.checkComponentPermission(activityInfo.permission, 726 android.os.Process.myUid(), activityInfo.applicationInfo.uid, 727 activityInfo.exported) == PackageManager.PERMISSION_GRANTED) { 728 mActivities.add(new ActivityResolveInfo(resolveInfo)); 729 } 730 } 731 return true; 732 } 733 return false; 734 } 735 736 /** 737 * Reads the historical data if necessary which is it has 738 * changed, there is a history file, and there is not persist 739 * in progress. 740 * 741 * @return Whether reading was performed. 742 */ readHistoricalDataIfNeeded()743 private boolean readHistoricalDataIfNeeded() { 744 if (mCanReadHistoricalData && mHistoricalRecordsChanged && 745 !TextUtils.isEmpty(mHistoryFileName)) { 746 mCanReadHistoricalData = false; 747 mReadShareHistoryCalled = true; 748 readHistoricalDataImpl(); 749 return true; 750 } 751 return false; 752 } 753 754 /** 755 * Adds a historical record. 756 * 757 * @param historicalRecord The record to add. 758 * @return True if the record was added. 759 */ addHisoricalRecord(HistoricalRecord historicalRecord)760 private boolean addHisoricalRecord(HistoricalRecord historicalRecord) { 761 final boolean added = mHistoricalRecords.add(historicalRecord); 762 if (added) { 763 mHistoricalRecordsChanged = true; 764 pruneExcessiveHistoricalRecordsIfNeeded(); 765 persistHistoricalDataIfNeeded(); 766 sortActivitiesIfNeeded(); 767 notifyChanged(); 768 } 769 return added; 770 } 771 772 /** 773 * Prunes older excessive records to guarantee maxHistorySize. 774 */ pruneExcessiveHistoricalRecordsIfNeeded()775 private void pruneExcessiveHistoricalRecordsIfNeeded() { 776 final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize; 777 if (pruneCount <= 0) { 778 return; 779 } 780 mHistoricalRecordsChanged = true; 781 for (int i = 0; i < pruneCount; i++) { 782 HistoricalRecord prunedRecord = mHistoricalRecords.remove(0); 783 if (DEBUG) { 784 Log.i(LOG_TAG, "Pruned: " + prunedRecord); 785 } 786 } 787 } 788 789 /** 790 * Represents a record in the history. 791 */ 792 public final static class HistoricalRecord { 793 794 /** 795 * The activity name. 796 */ 797 public final ComponentName activity; 798 799 /** 800 * The choice time. 801 */ 802 public final long time; 803 804 /** 805 * The record weight. 806 */ 807 public final float weight; 808 809 /** 810 * Creates a new instance. 811 * 812 * @param activityName The activity component name flattened to string. 813 * @param time The time the activity was chosen. 814 * @param weight The weight of the record. 815 */ HistoricalRecord(String activityName, long time, float weight)816 public HistoricalRecord(String activityName, long time, float weight) { 817 this(ComponentName.unflattenFromString(activityName), time, weight); 818 } 819 820 /** 821 * Creates a new instance. 822 * 823 * @param activityName The activity name. 824 * @param time The time the activity was chosen. 825 * @param weight The weight of the record. 826 */ HistoricalRecord(ComponentName activityName, long time, float weight)827 public HistoricalRecord(ComponentName activityName, long time, float weight) { 828 this.activity = activityName; 829 this.time = time; 830 this.weight = weight; 831 } 832 833 @Override hashCode()834 public int hashCode() { 835 final int prime = 31; 836 int result = 1; 837 result = prime * result + ((activity == null) ? 0 : activity.hashCode()); 838 result = prime * result + (int) (time ^ (time >>> 32)); 839 result = prime * result + Float.floatToIntBits(weight); 840 return result; 841 } 842 843 @Override equals(@ullable Object obj)844 public boolean equals(@Nullable Object obj) { 845 if (this == obj) { 846 return true; 847 } 848 if (obj == null) { 849 return false; 850 } 851 if (getClass() != obj.getClass()) { 852 return false; 853 } 854 HistoricalRecord other = (HistoricalRecord) obj; 855 if (activity == null) { 856 if (other.activity != null) { 857 return false; 858 } 859 } else if (!activity.equals(other.activity)) { 860 return false; 861 } 862 if (time != other.time) { 863 return false; 864 } 865 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { 866 return false; 867 } 868 return true; 869 } 870 871 @Override toString()872 public String toString() { 873 StringBuilder builder = new StringBuilder(); 874 builder.append("["); 875 builder.append("; activity:").append(activity); 876 builder.append("; time:").append(time); 877 builder.append("; weight:").append(new BigDecimal(weight)); 878 builder.append("]"); 879 return builder.toString(); 880 } 881 } 882 883 /** 884 * Represents an activity. 885 */ 886 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> { 887 888 /** 889 * The {@link ResolveInfo} of the activity. 890 */ 891 public final ResolveInfo resolveInfo; 892 893 /** 894 * Weight of the activity. Useful for sorting. 895 */ 896 public float weight; 897 898 /** 899 * Creates a new instance. 900 * 901 * @param resolveInfo activity {@link ResolveInfo}. 902 */ ActivityResolveInfo(ResolveInfo resolveInfo)903 public ActivityResolveInfo(ResolveInfo resolveInfo) { 904 this.resolveInfo = resolveInfo; 905 } 906 907 @Override hashCode()908 public int hashCode() { 909 return 31 + Float.floatToIntBits(weight); 910 } 911 912 @Override equals(@ullable Object obj)913 public boolean equals(@Nullable Object obj) { 914 if (this == obj) { 915 return true; 916 } 917 if (obj == null) { 918 return false; 919 } 920 if (getClass() != obj.getClass()) { 921 return false; 922 } 923 ActivityResolveInfo other = (ActivityResolveInfo) obj; 924 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { 925 return false; 926 } 927 return true; 928 } 929 compareTo(ActivityResolveInfo another)930 public int compareTo(ActivityResolveInfo another) { 931 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight); 932 } 933 934 @Override toString()935 public String toString() { 936 StringBuilder builder = new StringBuilder(); 937 builder.append("["); 938 builder.append("resolveInfo:").append(resolveInfo.toString()); 939 builder.append("; weight:").append(new BigDecimal(weight)); 940 builder.append("]"); 941 return builder.toString(); 942 } 943 } 944 945 /** 946 * Default activity sorter implementation. 947 */ 948 private final class DefaultSorter implements ActivitySorter { 949 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f; 950 951 private final Map<ComponentName, ActivityResolveInfo> mPackageNameToActivityMap = 952 new HashMap<ComponentName, ActivityResolveInfo>(); 953 sort(Intent intent, List<ActivityResolveInfo> activities, List<HistoricalRecord> historicalRecords)954 public void sort(Intent intent, List<ActivityResolveInfo> activities, 955 List<HistoricalRecord> historicalRecords) { 956 Map<ComponentName, ActivityResolveInfo> componentNameToActivityMap = 957 mPackageNameToActivityMap; 958 componentNameToActivityMap.clear(); 959 960 final int activityCount = activities.size(); 961 for (int i = 0; i < activityCount; i++) { 962 ActivityResolveInfo activity = activities.get(i); 963 activity.weight = 0.0f; 964 ComponentName componentName = new ComponentName( 965 activity.resolveInfo.activityInfo.packageName, 966 activity.resolveInfo.activityInfo.name); 967 componentNameToActivityMap.put(componentName, activity); 968 } 969 970 final int lastShareIndex = historicalRecords.size() - 1; 971 float nextRecordWeight = 1; 972 for (int i = lastShareIndex; i >= 0; i--) { 973 HistoricalRecord historicalRecord = historicalRecords.get(i); 974 ComponentName componentName = historicalRecord.activity; 975 ActivityResolveInfo activity = componentNameToActivityMap.get(componentName); 976 if (activity != null) { 977 activity.weight += historicalRecord.weight * nextRecordWeight; 978 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT; 979 } 980 } 981 982 Collections.sort(activities); 983 984 if (DEBUG) { 985 for (int i = 0; i < activityCount; i++) { 986 Log.i(LOG_TAG, "Sorted: " + activities.get(i)); 987 } 988 } 989 } 990 } 991 readHistoricalDataImpl()992 private void readHistoricalDataImpl() { 993 FileInputStream fis = null; 994 try { 995 fis = mContext.openFileInput(mHistoryFileName); 996 } catch (FileNotFoundException fnfe) { 997 if (DEBUG) { 998 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName); 999 } 1000 return; 1001 } 1002 try { 1003 XmlPullParser parser = Xml.newPullParser(); 1004 parser.setInput(fis, StandardCharsets.UTF_8.name()); 1005 1006 int type = XmlPullParser.START_DOCUMENT; 1007 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { 1008 type = parser.next(); 1009 } 1010 1011 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) { 1012 throw new XmlPullParserException("Share records file does not start with " 1013 + TAG_HISTORICAL_RECORDS + " tag."); 1014 } 1015 1016 List<HistoricalRecord> historicalRecords = mHistoricalRecords; 1017 historicalRecords.clear(); 1018 1019 while (true) { 1020 type = parser.next(); 1021 if (type == XmlPullParser.END_DOCUMENT) { 1022 break; 1023 } 1024 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 1025 continue; 1026 } 1027 String nodeName = parser.getName(); 1028 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) { 1029 throw new XmlPullParserException("Share records file not well-formed."); 1030 } 1031 1032 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY); 1033 final long time = 1034 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME)); 1035 final float weight = 1036 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT)); 1037 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight); 1038 historicalRecords.add(readRecord); 1039 1040 if (DEBUG) { 1041 Log.i(LOG_TAG, "Read " + readRecord.toString()); 1042 } 1043 } 1044 1045 if (DEBUG) { 1046 Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records."); 1047 } 1048 } catch (XmlPullParserException xppe) { 1049 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe); 1050 } catch (IOException ioe) { 1051 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe); 1052 } finally { 1053 if (fis != null) { 1054 try { 1055 fis.close(); 1056 } catch (IOException ioe) { 1057 /* ignore */ 1058 } 1059 } 1060 } 1061 } 1062 1063 /** 1064 * Command for persisting the historical records to a file off the UI thread. 1065 */ 1066 private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> { 1067 1068 @Override 1069 @SuppressWarnings("unchecked") doInBackground(Object... args)1070 public Void doInBackground(Object... args) { 1071 List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0]; 1072 String hostoryFileName = (String) args[1]; 1073 1074 FileOutputStream fos = null; 1075 1076 try { 1077 fos = mContext.openFileOutput(hostoryFileName, Context.MODE_PRIVATE); 1078 } catch (FileNotFoundException fnfe) { 1079 Log.e(LOG_TAG, "Error writing historical recrod file: " + hostoryFileName, fnfe); 1080 return null; 1081 } 1082 1083 XmlSerializer serializer = Xml.newSerializer(); 1084 1085 try { 1086 serializer.setOutput(fos, null); 1087 serializer.startDocument(StandardCharsets.UTF_8.name(), true); 1088 serializer.startTag(null, TAG_HISTORICAL_RECORDS); 1089 1090 final int recordCount = historicalRecords.size(); 1091 for (int i = 0; i < recordCount; i++) { 1092 HistoricalRecord record = historicalRecords.remove(0); 1093 serializer.startTag(null, TAG_HISTORICAL_RECORD); 1094 serializer.attribute(null, ATTRIBUTE_ACTIVITY, 1095 record.activity.flattenToString()); 1096 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time)); 1097 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight)); 1098 serializer.endTag(null, TAG_HISTORICAL_RECORD); 1099 if (DEBUG) { 1100 Log.i(LOG_TAG, "Wrote " + record.toString()); 1101 } 1102 } 1103 1104 serializer.endTag(null, TAG_HISTORICAL_RECORDS); 1105 serializer.endDocument(); 1106 1107 if (DEBUG) { 1108 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records."); 1109 } 1110 } catch (IllegalArgumentException iae) { 1111 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae); 1112 } catch (IllegalStateException ise) { 1113 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise); 1114 } catch (IOException ioe) { 1115 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe); 1116 } finally { 1117 mCanReadHistoricalData = true; 1118 if (fos != null) { 1119 try { 1120 fos.close(); 1121 } catch (IOException e) { 1122 /* ignore */ 1123 } 1124 } 1125 } 1126 return null; 1127 } 1128 } 1129 1130 /** 1131 * Keeps in sync the historical records and activities with the installed applications. 1132 */ 1133 private final class DataModelPackageMonitor extends PackageMonitor { 1134 1135 @Override onSomePackagesChanged()1136 public void onSomePackagesChanged() { 1137 mReloadActivities = true; 1138 } 1139 } 1140 } 1141