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