Home | History | Annotate | Download | only in widget
      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          */
    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.
    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          */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    654     public int getHistorySize() {
    655         synchronized (mInstanceLock) {
    656             ensureConsistentState();
    657             return mHistoricalRecords.size();
    658         }
    659     }
    660 
    661     @Override
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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          */
    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          */
    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
    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
    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
    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          */
    894         public ActivityResolveInfo(ResolveInfo resolveInfo) {
    895             this.resolveInfo = resolveInfo;
    896         }
    897 
    898         @Override
    899         public int hashCode() {
    900             return 31 + Float.floatToIntBits(weight);
    901         }
    902 
    903         @Override
    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 
    921         public int compareTo(ActivityResolveInfo another) {
    922              return  Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
    923         }
    924 
    925         @Override
    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 
    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 
    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")
   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
   1127         public void onSomePackagesChanged() {
   1128             mReloadActivities = true;
   1129         }
   1130     }
   1131 }
   1132