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