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