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