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