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.nio.charset.StandardCharsets; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 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 */ 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 * 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 */ 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> mActivities = 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 #readHistoricalDataIfNeeded()} 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 #persistHistoricalDataIfNeeded()} 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 #persistHistoricalDataIfNeeded()} a call to 292 * {@link #persistHistoricalDataIfNeeded()} 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 #persistHistoricalDataIfNeeded()} 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 * Flag whether to reload the activities for the current intent. 310 */ 311 private boolean mReloadActivities = false; 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 */ 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 return dataModel; 350 } 351 } 352 353 /** 354 * Creates a new instance. 355 * 356 * @param context Context for loading resources. 357 * @param historyFileName The history XML file. 358 */ 359 private ActivityChooserModel(Context context, String historyFileName) { 360 mContext = context.getApplicationContext(); 361 if (!TextUtils.isEmpty(historyFileName) 362 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) { 363 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION; 364 } else { 365 mHistoryFileName = historyFileName; 366 } 367 mPackageMonitor.register(mContext, null, true); 368 } 369 370 /** 371 * Sets an intent for which to choose a activity. 372 * <p> 373 * <strong>Note:</strong> Clients must set only semantically similar 374 * intents for each data model. 375 * <p> 376 * 377 * @param intent The intent. 378 */ 379 public void setIntent(Intent intent) { 380 synchronized (mInstanceLock) { 381 if (mIntent == intent) { 382 return; 383 } 384 mIntent = intent; 385 mReloadActivities = true; 386 ensureConsistentState(); 387 } 388 } 389 390 /** 391 * Gets the intent for which a activity is being chosen. 392 * 393 * @return The intent. 394 */ 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 */ 408 public int getActivityCount() { 409 synchronized (mInstanceLock) { 410 ensureConsistentState(); 411 return mActivities.size(); 412 } 413 } 414 415 /** 416 * Gets an activity at a given index. 417 * 418 * @return The activity. 419 * 420 * @see ActivityResolveInfo 421 * @see #setIntent(Intent) 422 */ 423 public ResolveInfo getActivity(int index) { 424 synchronized (mInstanceLock) { 425 ensureConsistentState(); 426 return mActivities.get(index).resolveInfo; 427 } 428 } 429 430 /** 431 * Gets the index of a the given activity. 432 * 433 * @param activity The activity index. 434 * 435 * @return The index if found, -1 otherwise. 436 */ 437 public int getActivityIndex(ResolveInfo activity) { 438 synchronized (mInstanceLock) { 439 ensureConsistentState(); 440 List<ActivityResolveInfo> activities = mActivities; 441 final int activityCount = activities.size(); 442 for (int i = 0; i < activityCount; i++) { 443 ActivityResolveInfo currentActivity = activities.get(i); 444 if (currentActivity.resolveInfo == activity) { 445 return i; 446 } 447 } 448 return INVALID_INDEX; 449 } 450 } 451 452 /** 453 * Chooses a activity to handle the current intent. This will result in 454 * adding a historical record for that action and construct intent with 455 * its component name set such that it can be immediately started by the 456 * client. 457 * <p> 458 * <strong>Note:</strong> By calling this method the client guarantees 459 * that the returned intent will be started. This intent is returned to 460 * the client solely to let additional customization before the start. 461 * </p> 462 * 463 * @return An {@link Intent} for launching the activity or null if the 464 * policy has consumed the intent or there is not current intent 465 * set via {@link #setIntent(Intent)}. 466 * 467 * @see HistoricalRecord 468 * @see OnChooseActivityListener 469 */ 470 public Intent chooseActivity(int index) { 471 synchronized (mInstanceLock) { 472 if (mIntent == null) { 473 return null; 474 } 475 476 ensureConsistentState(); 477 478 ActivityResolveInfo chosenActivity = mActivities.get(index); 479 480 ComponentName chosenName = new ComponentName( 481 chosenActivity.resolveInfo.activityInfo.packageName, 482 chosenActivity.resolveInfo.activityInfo.name); 483 484 Intent choiceIntent = new Intent(mIntent); 485 choiceIntent.setComponent(chosenName); 486 487 if (mActivityChoserModelPolicy != null) { 488 // Do not allow the policy to change the intent. 489 Intent choiceIntentCopy = new Intent(choiceIntent); 490 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this, 491 choiceIntentCopy); 492 if (handled) { 493 return null; 494 } 495 } 496 497 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName, 498 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT); 499 addHisoricalRecord(historicalRecord); 500 501 return choiceIntent; 502 } 503 } 504 505 /** 506 * Sets the listener for choosing an activity. 507 * 508 * @param listener The listener. 509 */ 510 public void setOnChooseActivityListener(OnChooseActivityListener listener) { 511 synchronized (mInstanceLock) { 512 mActivityChoserModelPolicy = listener; 513 } 514 } 515 516 /** 517 * Gets the default activity, The default activity is defined as the one 518 * with highest rank i.e. the first one in the list of activities that can 519 * handle the intent. 520 * 521 * @return The default activity, <code>null</code> id not activities. 522 * 523 * @see #getActivity(int) 524 */ 525 public ResolveInfo getDefaultActivity() { 526 synchronized (mInstanceLock) { 527 ensureConsistentState(); 528 if (!mActivities.isEmpty()) { 529 return mActivities.get(0).resolveInfo; 530 } 531 } 532 return null; 533 } 534 535 /** 536 * Sets the default activity. The default activity is set by adding a 537 * historical record with weight high enough that this activity will 538 * become the highest ranked. Such a strategy guarantees that the default 539 * will eventually change if not used. Also the weight of the record for 540 * setting a default is inflated with a constant amount to guarantee that 541 * it will stay as default for awhile. 542 * 543 * @param index The index of the activity to set as default. 544 */ 545 public void setDefaultActivity(int index) { 546 synchronized (mInstanceLock) { 547 ensureConsistentState(); 548 549 ActivityResolveInfo newDefaultActivity = mActivities.get(index); 550 ActivityResolveInfo oldDefaultActivity = mActivities.get(0); 551 552 final float weight; 553 if (oldDefaultActivity != null) { 554 // Add a record with weight enough to boost the chosen at the top. 555 weight = oldDefaultActivity.weight - newDefaultActivity.weight 556 + DEFAULT_ACTIVITY_INFLATION; 557 } else { 558 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT; 559 } 560 561 ComponentName defaultName = new ComponentName( 562 newDefaultActivity.resolveInfo.activityInfo.packageName, 563 newDefaultActivity.resolveInfo.activityInfo.name); 564 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName, 565 System.currentTimeMillis(), weight); 566 addHisoricalRecord(historicalRecord); 567 } 568 } 569 570 /** 571 * Persists the history data to the backing file if the latter 572 * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()} 573 * throws an exception. Calling this method more than one without choosing an 574 * activity has not effect. 575 * 576 * @throws IllegalStateException If this method is called before a call to 577 * {@link #readHistoricalDataIfNeeded()}. 578 */ 579 private void persistHistoricalDataIfNeeded() { 580 if (!mReadShareHistoryCalled) { 581 throw new IllegalStateException("No preceding call to #readHistoricalData"); 582 } 583 if (!mHistoricalRecordsChanged) { 584 return; 585 } 586 mHistoricalRecordsChanged = false; 587 if (!TextUtils.isEmpty(mHistoryFileName)) { 588 new PersistHistoryAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, 589 new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName); 590 } 591 } 592 593 /** 594 * Sets the sorter for ordering activities based on historical data and an intent. 595 * 596 * @param activitySorter The sorter. 597 * 598 * @see ActivitySorter 599 */ 600 public void setActivitySorter(ActivitySorter activitySorter) { 601 synchronized (mInstanceLock) { 602 if (mActivitySorter == activitySorter) { 603 return; 604 } 605 mActivitySorter = activitySorter; 606 if (sortActivitiesIfNeeded()) { 607 notifyChanged(); 608 } 609 } 610 } 611 612 /** 613 * Sets the maximal size of the historical data. Defaults to 614 * {@link #DEFAULT_HISTORY_MAX_LENGTH} 615 * <p> 616 * <strong>Note:</strong> Setting this property will immediately 617 * enforce the specified max history size by dropping enough old 618 * historical records to enforce the desired size. Thus, any 619 * records that exceed the history size will be discarded and 620 * irreversibly lost. 621 * </p> 622 * 623 * @param historyMaxSize The max history size. 624 */ 625 public void setHistoryMaxSize(int historyMaxSize) { 626 synchronized (mInstanceLock) { 627 if (mHistoryMaxSize == historyMaxSize) { 628 return; 629 } 630 mHistoryMaxSize = historyMaxSize; 631 pruneExcessiveHistoricalRecordsIfNeeded(); 632 if (sortActivitiesIfNeeded()) { 633 notifyChanged(); 634 } 635 } 636 } 637 638 /** 639 * Gets the history max size. 640 * 641 * @return The history max size. 642 */ 643 public int getHistoryMaxSize() { 644 synchronized (mInstanceLock) { 645 return mHistoryMaxSize; 646 } 647 } 648 649 /** 650 * Gets the history size. 651 * 652 * @return The history size. 653 */ 654 public int getHistorySize() { 655 synchronized (mInstanceLock) { 656 ensureConsistentState(); 657 return mHistoricalRecords.size(); 658 } 659 } 660 661 @Override 662 protected void finalize() throws Throwable { 663 super.finalize(); 664 mPackageMonitor.unregister(); 665 } 666 667 /** 668 * Ensures the model is in a consistent state which is the 669 * activities for the current intent have been loaded, the 670 * most recent history has been read, and the activities 671 * are sorted. 672 */ 673 private void ensureConsistentState() { 674 boolean stateChanged = loadActivitiesIfNeeded(); 675 stateChanged |= readHistoricalDataIfNeeded(); 676 pruneExcessiveHistoricalRecordsIfNeeded(); 677 if (stateChanged) { 678 sortActivitiesIfNeeded(); 679 notifyChanged(); 680 } 681 } 682 683 /** 684 * Sorts the activities if necessary which is if there is a 685 * sorter, there are some activities to sort, and there is some 686 * historical data. 687 * 688 * @return Whether sorting was performed. 689 */ 690 private boolean sortActivitiesIfNeeded() { 691 if (mActivitySorter != null && mIntent != null 692 && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) { 693 mActivitySorter.sort(mIntent, mActivities, 694 Collections.unmodifiableList(mHistoricalRecords)); 695 return true; 696 } 697 return false; 698 } 699 700 /** 701 * Loads the activities for the current intent if needed which is 702 * if they are not already loaded for the current intent. 703 * 704 * @return Whether loading was performed. 705 */ 706 private boolean loadActivitiesIfNeeded() { 707 if (mReloadActivities && mIntent != null) { 708 mReloadActivities = false; 709 mActivities.clear(); 710 List<ResolveInfo> resolveInfos = mContext.getPackageManager() 711 .queryIntentActivities(mIntent, 0); 712 final int resolveInfoCount = resolveInfos.size(); 713 for (int i = 0; i < resolveInfoCount; i++) { 714 ResolveInfo resolveInfo = resolveInfos.get(i); 715 ActivityInfo activityInfo = resolveInfo.activityInfo; 716 if (ActivityManager.checkComponentPermission(activityInfo.permission, 717 android.os.Process.myUid(), activityInfo.applicationInfo.uid, 718 activityInfo.exported) == PackageManager.PERMISSION_GRANTED) { 719 mActivities.add(new ActivityResolveInfo(resolveInfo)); 720 } 721 } 722 return true; 723 } 724 return false; 725 } 726 727 /** 728 * Reads the historical data if necessary which is it has 729 * changed, there is a history file, and there is not persist 730 * in progress. 731 * 732 * @return Whether reading was performed. 733 */ 734 private boolean readHistoricalDataIfNeeded() { 735 if (mCanReadHistoricalData && mHistoricalRecordsChanged && 736 !TextUtils.isEmpty(mHistoryFileName)) { 737 mCanReadHistoricalData = false; 738 mReadShareHistoryCalled = true; 739 readHistoricalDataImpl(); 740 return true; 741 } 742 return false; 743 } 744 745 /** 746 * Adds a historical record. 747 * 748 * @param historicalRecord The record to add. 749 * @return True if the record was added. 750 */ 751 private boolean addHisoricalRecord(HistoricalRecord historicalRecord) { 752 final boolean added = mHistoricalRecords.add(historicalRecord); 753 if (added) { 754 mHistoricalRecordsChanged = true; 755 pruneExcessiveHistoricalRecordsIfNeeded(); 756 persistHistoricalDataIfNeeded(); 757 sortActivitiesIfNeeded(); 758 notifyChanged(); 759 } 760 return added; 761 } 762 763 /** 764 * Prunes older excessive records to guarantee maxHistorySize. 765 */ 766 private void pruneExcessiveHistoricalRecordsIfNeeded() { 767 final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize; 768 if (pruneCount <= 0) { 769 return; 770 } 771 mHistoricalRecordsChanged = true; 772 for (int i = 0; i < pruneCount; i++) { 773 HistoricalRecord prunedRecord = mHistoricalRecords.remove(0); 774 if (DEBUG) { 775 Log.i(LOG_TAG, "Pruned: " + prunedRecord); 776 } 777 } 778 } 779 780 /** 781 * Represents a record in the history. 782 */ 783 public final static class HistoricalRecord { 784 785 /** 786 * The activity name. 787 */ 788 public final ComponentName activity; 789 790 /** 791 * The choice time. 792 */ 793 public final long time; 794 795 /** 796 * The record weight. 797 */ 798 public final float weight; 799 800 /** 801 * Creates a new instance. 802 * 803 * @param activityName The activity component name flattened to string. 804 * @param time The time the activity was chosen. 805 * @param weight The weight of the record. 806 */ 807 public HistoricalRecord(String activityName, long time, float weight) { 808 this(ComponentName.unflattenFromString(activityName), time, weight); 809 } 810 811 /** 812 * Creates a new instance. 813 * 814 * @param activityName The activity name. 815 * @param time The time the activity was chosen. 816 * @param weight The weight of the record. 817 */ 818 public HistoricalRecord(ComponentName activityName, long time, float weight) { 819 this.activity = activityName; 820 this.time = time; 821 this.weight = weight; 822 } 823 824 @Override 825 public int hashCode() { 826 final int prime = 31; 827 int result = 1; 828 result = prime * result + ((activity == null) ? 0 : activity.hashCode()); 829 result = prime * result + (int) (time ^ (time >>> 32)); 830 result = prime * result + Float.floatToIntBits(weight); 831 return result; 832 } 833 834 @Override 835 public boolean equals(Object obj) { 836 if (this == obj) { 837 return true; 838 } 839 if (obj == null) { 840 return false; 841 } 842 if (getClass() != obj.getClass()) { 843 return false; 844 } 845 HistoricalRecord other = (HistoricalRecord) obj; 846 if (activity == null) { 847 if (other.activity != null) { 848 return false; 849 } 850 } else if (!activity.equals(other.activity)) { 851 return false; 852 } 853 if (time != other.time) { 854 return false; 855 } 856 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { 857 return false; 858 } 859 return true; 860 } 861 862 @Override 863 public String toString() { 864 StringBuilder builder = new StringBuilder(); 865 builder.append("["); 866 builder.append("; activity:").append(activity); 867 builder.append("; time:").append(time); 868 builder.append("; weight:").append(new BigDecimal(weight)); 869 builder.append("]"); 870 return builder.toString(); 871 } 872 } 873 874 /** 875 * Represents an activity. 876 */ 877 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> { 878 879 /** 880 * The {@link ResolveInfo} of the activity. 881 */ 882 public final ResolveInfo resolveInfo; 883 884 /** 885 * Weight of the activity. Useful for sorting. 886 */ 887 public float weight; 888 889 /** 890 * Creates a new instance. 891 * 892 * @param resolveInfo activity {@link ResolveInfo}. 893 */ 894 public ActivityResolveInfo(ResolveInfo resolveInfo) { 895 this.resolveInfo = resolveInfo; 896 } 897 898 @Override 899 public int hashCode() { 900 return 31 + Float.floatToIntBits(weight); 901 } 902 903 @Override 904 public boolean equals(Object obj) { 905 if (this == obj) { 906 return true; 907 } 908 if (obj == null) { 909 return false; 910 } 911 if (getClass() != obj.getClass()) { 912 return false; 913 } 914 ActivityResolveInfo other = (ActivityResolveInfo) obj; 915 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { 916 return false; 917 } 918 return true; 919 } 920 921 public int compareTo(ActivityResolveInfo another) { 922 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight); 923 } 924 925 @Override 926 public String toString() { 927 StringBuilder builder = new StringBuilder(); 928 builder.append("["); 929 builder.append("resolveInfo:").append(resolveInfo.toString()); 930 builder.append("; weight:").append(new BigDecimal(weight)); 931 builder.append("]"); 932 return builder.toString(); 933 } 934 } 935 936 /** 937 * Default activity sorter implementation. 938 */ 939 private final class DefaultSorter implements ActivitySorter { 940 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f; 941 942 private final Map<ComponentName, ActivityResolveInfo> mPackageNameToActivityMap = 943 new HashMap<ComponentName, ActivityResolveInfo>(); 944 945 public void sort(Intent intent, List<ActivityResolveInfo> activities, 946 List<HistoricalRecord> historicalRecords) { 947 Map<ComponentName, ActivityResolveInfo> componentNameToActivityMap = 948 mPackageNameToActivityMap; 949 componentNameToActivityMap.clear(); 950 951 final int activityCount = activities.size(); 952 for (int i = 0; i < activityCount; i++) { 953 ActivityResolveInfo activity = activities.get(i); 954 activity.weight = 0.0f; 955 ComponentName componentName = new ComponentName( 956 activity.resolveInfo.activityInfo.packageName, 957 activity.resolveInfo.activityInfo.name); 958 componentNameToActivityMap.put(componentName, activity); 959 } 960 961 final int lastShareIndex = historicalRecords.size() - 1; 962 float nextRecordWeight = 1; 963 for (int i = lastShareIndex; i >= 0; i--) { 964 HistoricalRecord historicalRecord = historicalRecords.get(i); 965 ComponentName componentName = historicalRecord.activity; 966 ActivityResolveInfo activity = componentNameToActivityMap.get(componentName); 967 if (activity != null) { 968 activity.weight += historicalRecord.weight * nextRecordWeight; 969 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT; 970 } 971 } 972 973 Collections.sort(activities); 974 975 if (DEBUG) { 976 for (int i = 0; i < activityCount; i++) { 977 Log.i(LOG_TAG, "Sorted: " + activities.get(i)); 978 } 979 } 980 } 981 } 982 983 private void readHistoricalDataImpl() { 984 FileInputStream fis = null; 985 try { 986 fis = mContext.openFileInput(mHistoryFileName); 987 } catch (FileNotFoundException fnfe) { 988 if (DEBUG) { 989 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName); 990 } 991 return; 992 } 993 try { 994 XmlPullParser parser = Xml.newPullParser(); 995 parser.setInput(fis, StandardCharsets.UTF_8.name()); 996 997 int type = XmlPullParser.START_DOCUMENT; 998 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { 999 type = parser.next(); 1000 } 1001 1002 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) { 1003 throw new XmlPullParserException("Share records file does not start with " 1004 + TAG_HISTORICAL_RECORDS + " tag."); 1005 } 1006 1007 List<HistoricalRecord> historicalRecords = mHistoricalRecords; 1008 historicalRecords.clear(); 1009 1010 while (true) { 1011 type = parser.next(); 1012 if (type == XmlPullParser.END_DOCUMENT) { 1013 break; 1014 } 1015 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 1016 continue; 1017 } 1018 String nodeName = parser.getName(); 1019 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) { 1020 throw new XmlPullParserException("Share records file not well-formed."); 1021 } 1022 1023 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY); 1024 final long time = 1025 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME)); 1026 final float weight = 1027 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT)); 1028 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight); 1029 historicalRecords.add(readRecord); 1030 1031 if (DEBUG) { 1032 Log.i(LOG_TAG, "Read " + readRecord.toString()); 1033 } 1034 } 1035 1036 if (DEBUG) { 1037 Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records."); 1038 } 1039 } catch (XmlPullParserException xppe) { 1040 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe); 1041 } catch (IOException ioe) { 1042 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe); 1043 } finally { 1044 if (fis != null) { 1045 try { 1046 fis.close(); 1047 } catch (IOException ioe) { 1048 /* ignore */ 1049 } 1050 } 1051 } 1052 } 1053 1054 /** 1055 * Command for persisting the historical records to a file off the UI thread. 1056 */ 1057 private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> { 1058 1059 @Override 1060 @SuppressWarnings("unchecked") 1061 public Void doInBackground(Object... args) { 1062 List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0]; 1063 String hostoryFileName = (String) args[1]; 1064 1065 FileOutputStream fos = null; 1066 1067 try { 1068 fos = mContext.openFileOutput(hostoryFileName, Context.MODE_PRIVATE); 1069 } catch (FileNotFoundException fnfe) { 1070 Log.e(LOG_TAG, "Error writing historical recrod file: " + hostoryFileName, fnfe); 1071 return null; 1072 } 1073 1074 XmlSerializer serializer = Xml.newSerializer(); 1075 1076 try { 1077 serializer.setOutput(fos, null); 1078 serializer.startDocument(StandardCharsets.UTF_8.name(), true); 1079 serializer.startTag(null, TAG_HISTORICAL_RECORDS); 1080 1081 final int recordCount = historicalRecords.size(); 1082 for (int i = 0; i < recordCount; i++) { 1083 HistoricalRecord record = historicalRecords.remove(0); 1084 serializer.startTag(null, TAG_HISTORICAL_RECORD); 1085 serializer.attribute(null, ATTRIBUTE_ACTIVITY, 1086 record.activity.flattenToString()); 1087 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time)); 1088 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight)); 1089 serializer.endTag(null, TAG_HISTORICAL_RECORD); 1090 if (DEBUG) { 1091 Log.i(LOG_TAG, "Wrote " + record.toString()); 1092 } 1093 } 1094 1095 serializer.endTag(null, TAG_HISTORICAL_RECORDS); 1096 serializer.endDocument(); 1097 1098 if (DEBUG) { 1099 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records."); 1100 } 1101 } catch (IllegalArgumentException iae) { 1102 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae); 1103 } catch (IllegalStateException ise) { 1104 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise); 1105 } catch (IOException ioe) { 1106 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe); 1107 } finally { 1108 mCanReadHistoricalData = true; 1109 if (fos != null) { 1110 try { 1111 fos.close(); 1112 } catch (IOException e) { 1113 /* ignore */ 1114 } 1115 } 1116 } 1117 return null; 1118 } 1119 } 1120 1121 /** 1122 * Keeps in sync the historical records and activities with the installed applications. 1123 */ 1124 private final class DataModelPackageMonitor extends PackageMonitor { 1125 1126 @Override 1127 public void onSomePackagesChanged() { 1128 mReloadActivities = true; 1129 } 1130 } 1131 } 1132