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