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