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