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