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