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