FileDocCategorySizeDatePackage
ActivityChooserModel.javaAPI DocAndroid 5.1 API39467Thu Mar 12 22:22:10 GMT 2015android.widget

ActivityChooserModel

public class ActivityChooserModel extends android.database.DataSetObservable

This class represents a data model for choosing a component for handing a given {@link Intent}. The model is responsible for querying the system for activities that can handle the given intent and order found activities based on historical data of previous choices. The historical data is stored in an application private file. If a client does not want to have persistent choice history the file can be omitted, thus the activities will be ordered based on historical usage for the current session.

For each backing history file there is a singleton instance of this class. Thus, several clients that specify the same history file will share the same model. Note that if multiple clients are sharing the same model they should implement semantically equivalent functionality since setting the model intent will change the found activities and they may be inconsistent with the functionality of some of the clients. For example, choosing a share activity can be implemented by a single backing model and two different views for performing the selection. If however, one of the views is used for sharing but the other for importing, for example, then each view should be backed by a separate model.

The way clients interact with this class is as follows:


// Get a model and set it to a couple of clients with semantically similar function.
ActivityChooserModel dataModel =
ActivityChooserModel.get(context, "task_specific_history_file_name.xml");

ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
modelClient1.setActivityChooserModel(dataModel);

ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
modelClient2.setActivityChooserModel(dataModel);

// Set an intent to choose a an activity for.
dataModel.setIntent(intent);

Note: This class is thread safe.

hide

Fields Summary
private static final boolean
DEBUG
Flag for selecting debug mode.
private static final String
LOG_TAG
Tag used for logging.
private static final String
TAG_HISTORICAL_RECORDS
The root tag in the history file.
private static final String
TAG_HISTORICAL_RECORD
The tag for a record in the history file.
private static final String
ATTRIBUTE_ACTIVITY
Attribute for the activity.
private static final String
ATTRIBUTE_TIME
Attribute for the choice time.
private static final String
ATTRIBUTE_WEIGHT
Attribute for the choice weight.
public static final String
DEFAULT_HISTORY_FILE_NAME
The default name of the choice history file.
public static final int
DEFAULT_HISTORY_MAX_LENGTH
The default maximal length of the choice history.
private static final int
DEFAULT_ACTIVITY_INFLATION
The amount with which to inflate a chosen activity when set as default.
private static final float
DEFAULT_HISTORICAL_RECORD_WEIGHT
Default weight for a choice record.
private static final String
HISTORY_FILE_EXTENSION
The extension of the history file.
private static final int
INVALID_INDEX
An invalid item index.
private static final Object
sRegistryLock
Lock to guard the model registry.
private static final Map
sDataModelRegistry
This the registry for data models.
private final Object
mInstanceLock
Lock for synchronizing on this instance.
private final List
mActivities
List of activities that can handle the current intent.
private final List
mHistoricalRecords
List with historical choice records.
private final com.android.internal.content.PackageMonitor
mPackageMonitor
Monitor for added and removed packages.
private final android.content.Context
mContext
Context for accessing resources.
private final String
mHistoryFileName
The name of the history file that backs this model.
private android.content.Intent
mIntent
The intent for which a activity is being chosen.
private ActivitySorter
mActivitySorter
The sorter for ordering activities based on intent and past choices.
private int
mHistoryMaxSize
The maximal length of the choice history.
private boolean
mCanReadHistoricalData
Flag whether choice history can be read. In general many clients can share the same data model and {@link #readHistoricalDataIfNeeded()} may be called by arbitrary of them any number of times. Therefore, this class guarantees that the very first read succeeds and subsequent reads can be performed only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change of the share records.
private boolean
mReadShareHistoryCalled
Flag whether the choice history was read. This is used to enforce that before calling {@link #persistHistoricalDataIfNeeded()} a call to {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a scenario in which a choice history file exits, it is not read yet and it is overwritten. Note that always all historical records are read in full and the file is rewritten. This is necessary since we need to purge old records that are outside of the sliding window of past choices.
private boolean
mHistoricalRecordsChanged
Flag whether the choice records have changed. In general many clients can share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called by arbitrary of them any number of times. Therefore, this class guarantees that choice history will be persisted only if it has changed.
private boolean
mReloadActivities
Flag whether to reload the activities for the current intent.
private OnChooseActivityListener
mActivityChoserModelPolicy
Policy for controlling how the model handles chosen activities.
Constructors Summary
private ActivityChooserModel(android.content.Context context, String historyFileName)
Creates a new instance.

param
context Context for loading resources.
param
historyFileName The history XML file.

        mContext = context.getApplicationContext();
        if (!TextUtils.isEmpty(historyFileName)
                && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
            mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
        } else {
            mHistoryFileName = historyFileName;
        }
        mPackageMonitor.register(mContext, null, true);
    
Methods Summary
private booleanaddHisoricalRecord(android.widget.ActivityChooserModel$HistoricalRecord historicalRecord)
Adds a historical record.

param
historicalRecord The record to add.
return
True if the record was added.

        final boolean added = mHistoricalRecords.add(historicalRecord);
        if (added) {
            mHistoricalRecordsChanged = true;
            pruneExcessiveHistoricalRecordsIfNeeded();
            persistHistoricalDataIfNeeded();
            sortActivitiesIfNeeded();
            notifyChanged();
        }
        return added;
    
public android.content.IntentchooseActivity(int index)
Chooses a activity to handle the current intent. This will result in adding a historical record for that action and construct intent with its component name set such that it can be immediately started by the client.

Note: By calling this method the client guarantees that the returned intent will be started. This intent is returned to the client solely to let additional customization before the start.

return
An {@link Intent} for launching the activity or null if the policy has consumed the intent or there is not current intent set via {@link #setIntent(Intent)}.
see
HistoricalRecord
see
OnChooseActivityListener

        synchronized (mInstanceLock) {
            if (mIntent == null) {
                return null;
            }

            ensureConsistentState();

            ActivityResolveInfo chosenActivity = mActivities.get(index);

            ComponentName chosenName = new ComponentName(
                    chosenActivity.resolveInfo.activityInfo.packageName,
                    chosenActivity.resolveInfo.activityInfo.name);

            Intent choiceIntent = new Intent(mIntent);
            choiceIntent.setComponent(chosenName);

            if (mActivityChoserModelPolicy != null) {
                // Do not allow the policy to change the intent.
                Intent choiceIntentCopy = new Intent(choiceIntent);
                final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
                        choiceIntentCopy);
                if (handled) {
                    return null;
                }
            }

            HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
                    System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
            addHisoricalRecord(historicalRecord);

            return choiceIntent;
        }
    
private voidensureConsistentState()
Ensures the model is in a consistent state which is the activities for the current intent have been loaded, the most recent history has been read, and the activities are sorted.

        boolean stateChanged = loadActivitiesIfNeeded();
        stateChanged |= readHistoricalDataIfNeeded();
        pruneExcessiveHistoricalRecordsIfNeeded();
        if (stateChanged) {
            sortActivitiesIfNeeded();
            notifyChanged();
        }
    
protected voidfinalize()

        super.finalize();
        mPackageMonitor.unregister();
    
public static android.widget.ActivityChooserModelget(android.content.Context context, java.lang.String historyFileName)
Gets the data model backed by the contents of the provided file with historical data. Note that only one data model is backed by a given file, thus multiple calls with the same file name will return the same model instance. If no such instance is present it is created.

Note: To use the default historical data file clients should explicitly pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice history is desired clients should pass null for the file name. In such case a new model is returned for each invocation.

Always use difference historical data files for semantically different actions. For example, sharing is different from importing.

param
context Context for loading resources.
param
historyFileName File name with choice history, null if the model should not be backed by a file. In this case the activities will be ordered only by data from the current session.
return
The model.


                                                                                                                                                                                     
           
        synchronized (sRegistryLock) {
            ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
            if (dataModel == null) {
                dataModel = new ActivityChooserModel(context, historyFileName);
                sDataModelRegistry.put(historyFileName, dataModel);
            }
            return dataModel;
        }
    
public android.content.pm.ResolveInfogetActivity(int index)
Gets an activity at a given index.

return
The activity.
see
ActivityResolveInfo
see
#setIntent(Intent)

        synchronized (mInstanceLock) {
            ensureConsistentState();
            return mActivities.get(index).resolveInfo;
        }
    
public intgetActivityCount()
Gets the number of activities that can handle the intent.

return
The activity count.
see
#setIntent(Intent)

        synchronized (mInstanceLock) {
            ensureConsistentState();
            return mActivities.size();
        }
    
public intgetActivityIndex(android.content.pm.ResolveInfo activity)
Gets the index of a the given activity.

param
activity The activity index.
return
The index if found, -1 otherwise.

        synchronized (mInstanceLock) {
            ensureConsistentState();
            List<ActivityResolveInfo> activities = mActivities;
            final int activityCount = activities.size();
            for (int i = 0; i < activityCount; i++) {
                ActivityResolveInfo currentActivity = activities.get(i);
                if (currentActivity.resolveInfo == activity) {
                    return i;
                }
            }
            return INVALID_INDEX;
        }
    
public android.content.pm.ResolveInfogetDefaultActivity()
Gets the default activity, The default activity is defined as the one with highest rank i.e. the first one in the list of activities that can handle the intent.

return
The default activity, null id not activities.
see
#getActivity(int)

        synchronized (mInstanceLock) {
            ensureConsistentState();
            if (!mActivities.isEmpty()) {
                return mActivities.get(0).resolveInfo;
            }
        }
        return null;
    
public intgetHistoryMaxSize()
Gets the history max size.

return
The history max size.

        synchronized (mInstanceLock) {
            return mHistoryMaxSize;
        }
    
public intgetHistorySize()
Gets the history size.

return
The history size.

        synchronized (mInstanceLock) {
            ensureConsistentState();
            return mHistoricalRecords.size();
        }
    
public android.content.IntentgetIntent()
Gets the intent for which a activity is being chosen.

return
The intent.

        synchronized (mInstanceLock) {
            return mIntent;
        }
    
private booleanloadActivitiesIfNeeded()
Loads the activities for the current intent if needed which is if they are not already loaded for the current intent.

return
Whether loading was performed.

        if (mReloadActivities && mIntent != null) {
            mReloadActivities = false;
            mActivities.clear();
            List<ResolveInfo> resolveInfos = mContext.getPackageManager()
                    .queryIntentActivities(mIntent, 0);
            final int resolveInfoCount = resolveInfos.size();
            for (int i = 0; i < resolveInfoCount; i++) {
                ResolveInfo resolveInfo = resolveInfos.get(i);
                ActivityInfo activityInfo = resolveInfo.activityInfo;
                if (ActivityManager.checkComponentPermission(activityInfo.permission,
                        android.os.Process.myUid(), activityInfo.applicationInfo.uid,
                        activityInfo.exported) == PackageManager.PERMISSION_GRANTED) {
                    mActivities.add(new ActivityResolveInfo(resolveInfo));
                }
            }
            return true;
        }
        return false;
    
private voidpersistHistoricalDataIfNeeded()
Persists the history data to the backing file if the latter was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()} throws an exception. Calling this method more than one without choosing an activity has not effect.

throws
IllegalStateException If this method is called before a call to {@link #readHistoricalDataIfNeeded()}.

        if (!mReadShareHistoryCalled) {
            throw new IllegalStateException("No preceding call to #readHistoricalData");
        }
        if (!mHistoricalRecordsChanged) {
            return;
        }
        mHistoricalRecordsChanged = false;
        if (!TextUtils.isEmpty(mHistoryFileName)) {
            new PersistHistoryAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
                    new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
        }
    
private voidpruneExcessiveHistoricalRecordsIfNeeded()
Prunes older excessive records to guarantee maxHistorySize.

        final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
        if (pruneCount <= 0) {
            return;
        }
        mHistoricalRecordsChanged = true;
        for (int i = 0; i < pruneCount; i++) {
            HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
            if (DEBUG) {
                Log.i(LOG_TAG, "Pruned: " + prunedRecord);
            }
        }
    
private booleanreadHistoricalDataIfNeeded()
Reads the historical data if necessary which is it has changed, there is a history file, and there is not persist in progress.

return
Whether reading was performed.

        if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
                !TextUtils.isEmpty(mHistoryFileName)) {
            mCanReadHistoricalData = false;
            mReadShareHistoryCalled = true;
            readHistoricalDataImpl();
            return true;
        }
        return false;
    
private voidreadHistoricalDataImpl()

        FileInputStream fis = null;
        try {
            fis = mContext.openFileInput(mHistoryFileName);
        } catch (FileNotFoundException fnfe) {
            if (DEBUG) {
                Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
            }
            return;
        }
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(fis, null);

            int type = XmlPullParser.START_DOCUMENT;
            while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
                type = parser.next();
            }

            if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
                throw new XmlPullParserException("Share records file does not start with "
                        + TAG_HISTORICAL_RECORDS + " tag.");
            }

            List<HistoricalRecord> historicalRecords = mHistoricalRecords;
            historicalRecords.clear();

            while (true) {
                type = parser.next();
                if (type == XmlPullParser.END_DOCUMENT) {
                    break;
                }
                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                    continue;
                }
                String nodeName = parser.getName();
                if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
                    throw new XmlPullParserException("Share records file not well-formed.");
                }

                String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
                final long time =
                    Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
                final float weight =
                    Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
                 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
                historicalRecords.add(readRecord);

                if (DEBUG) {
                    Log.i(LOG_TAG, "Read " + readRecord.toString());
                }
            }

            if (DEBUG) {
                Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
            }
        } catch (XmlPullParserException xppe) {
            Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
        } catch (IOException ioe) {
            Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ioe) {
                    /* ignore */
                }
            }
        }
    
public voidsetActivitySorter(android.widget.ActivityChooserModel$ActivitySorter activitySorter)
Sets the sorter for ordering activities based on historical data and an intent.

param
activitySorter The sorter.
see
ActivitySorter

        synchronized (mInstanceLock) {
            if (mActivitySorter == activitySorter) {
                return;
            }
            mActivitySorter = activitySorter;
            if (sortActivitiesIfNeeded()) {
                notifyChanged();
            }
        }
    
public voidsetDefaultActivity(int index)
Sets the default activity. The default activity is set by adding a historical record with weight high enough that this activity will become the highest ranked. Such a strategy guarantees that the default will eventually change if not used. Also the weight of the record for setting a default is inflated with a constant amount to guarantee that it will stay as default for awhile.

param
index The index of the activity to set as default.

        synchronized (mInstanceLock) {
            ensureConsistentState();

            ActivityResolveInfo newDefaultActivity = mActivities.get(index);
            ActivityResolveInfo oldDefaultActivity = mActivities.get(0);

            final float weight;
            if (oldDefaultActivity != null) {
                // Add a record with weight enough to boost the chosen at the top.
                weight = oldDefaultActivity.weight - newDefaultActivity.weight
                    + DEFAULT_ACTIVITY_INFLATION;
            } else {
                weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
            }

            ComponentName defaultName = new ComponentName(
                    newDefaultActivity.resolveInfo.activityInfo.packageName,
                    newDefaultActivity.resolveInfo.activityInfo.name);
            HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
                    System.currentTimeMillis(), weight);
            addHisoricalRecord(historicalRecord);
        }
    
public voidsetHistoryMaxSize(int historyMaxSize)
Sets the maximal size of the historical data. Defaults to {@link #DEFAULT_HISTORY_MAX_LENGTH}

Note: Setting this property will immediately enforce the specified max history size by dropping enough old historical records to enforce the desired size. Thus, any records that exceed the history size will be discarded and irreversibly lost.

param
historyMaxSize The max history size.

        synchronized (mInstanceLock) {
            if (mHistoryMaxSize == historyMaxSize) {
                return;
            }
            mHistoryMaxSize = historyMaxSize;
            pruneExcessiveHistoricalRecordsIfNeeded();
            if (sortActivitiesIfNeeded()) {
                notifyChanged();
            }
        }
    
public voidsetIntent(android.content.Intent intent)
Sets an intent for which to choose a activity.

Note: Clients must set only semantically similar intents for each data model.

param
intent The intent.

        synchronized (mInstanceLock) {
            if (mIntent == intent) {
                return;
            }
            mIntent = intent;
            mReloadActivities = true;
            ensureConsistentState();
        }
    
public voidsetOnChooseActivityListener(android.widget.ActivityChooserModel$OnChooseActivityListener listener)
Sets the listener for choosing an activity.

param
listener The listener.

        synchronized (mInstanceLock) {
            mActivityChoserModelPolicy = listener;
        }
    
private booleansortActivitiesIfNeeded()
Sorts the activities if necessary which is if there is a sorter, there are some activities to sort, and there is some historical data.

return
Whether sorting was performed.

        if (mActivitySorter != null && mIntent != null
                && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
            mActivitySorter.sort(mIntent, mActivities,
                    Collections.unmodifiableList(mHistoricalRecords));
            return true;
        }
        return false;