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