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