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