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