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