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