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