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