blob: d87bdf482e4300b30c90d949f82162a677d89af8 [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.app.ActivityManager;
20import android.compat.annotation.UnsupportedAppUsage;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.ActivityInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.database.DataSetObservable;
28import android.os.AsyncTask;
29import android.text.TextUtils;
30import android.util.Log;
31import android.util.Xml;
32
33import com.android.internal.content.PackageMonitor;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37import org.xmlpull.v1.XmlSerializer;
38
39import java.io.FileInputStream;
40import java.io.FileNotFoundException;
41import java.io.FileOutputStream;
42import java.io.IOException;
43import java.math.BigDecimal;
44import java.nio.charset.StandardCharsets;
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.HashMap;
48import java.util.List;
49import 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 */
99public 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 */
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.
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 */
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
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 */
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
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 */
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
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
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 */
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
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
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 */
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 */
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 */
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 */
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 */
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 */
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 */
661 public int getHistorySize() {
662 synchronized (mInstanceLock) {
663 ensureConsistentState();
664 return mHistoricalRecords.size();
665 }
666 }
667
668 @Override
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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
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
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
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 */
901 public ActivityResolveInfo(ResolveInfo resolveInfo) {
902 this.resolveInfo = resolveInfo;
903 }
904
905 @Override
906 public int hashCode() {
907 return 31 + Float.floatToIntBits(weight);
908 }
909
910 @Override
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
928 public int compareTo(ActivityResolveInfo another) {
929 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
930 }
931
932 @Override
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
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
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")
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
1134 public void onSomePackagesChanged() {
1135 mReloadActivities = true;
1136 }
1137 }
1138}