Fields Summary |
---|
private static final boolean | DEBUGFlag for selecting debug mode. |
private static final String | LOG_TAGTag used for logging. |
private static final String | TAG_HISTORICAL_RECORDSThe root tag in the history file. |
private static final String | TAG_HISTORICAL_RECORDThe tag for a record in the history file. |
private static final String | ATTRIBUTE_ACTIVITYAttribute for the activity. |
private static final String | ATTRIBUTE_TIMEAttribute for the choice time. |
private static final String | ATTRIBUTE_WEIGHTAttribute for the choice weight. |
public static final String | DEFAULT_HISTORY_FILE_NAMEThe default name of the choice history file. |
public static final int | DEFAULT_HISTORY_MAX_LENGTHThe default maximal length of the choice history. |
private static final int | DEFAULT_ACTIVITY_INFLATIONThe amount with which to inflate a chosen activity when set as default. |
private static final float | DEFAULT_HISTORICAL_RECORD_WEIGHTDefault weight for a choice record. |
private static final String | HISTORY_FILE_EXTENSIONThe extension of the history file. |
private static final int | INVALID_INDEXAn invalid item index. |
private static final Object | sRegistryLockLock to guard the model registry. |
private static final Map | sDataModelRegistryThis the registry for data models. |
private final Object | mInstanceLockLock for synchronizing on this instance. |
private final List | mActivitiesList of activities that can handle the current intent. |
private final List | mHistoricalRecordsList with historical choice records. |
private final com.android.internal.content.PackageMonitor | mPackageMonitorMonitor for added and removed packages. |
private final android.content.Context | mContextContext for accessing resources. |
private final String | mHistoryFileNameThe name of the history file that backs this model. |
private android.content.Intent | mIntentThe intent for which a activity is being chosen. |
private ActivitySorter | mActivitySorterThe sorter for ordering activities based on intent and past choices. |
private int | mHistoryMaxSizeThe maximal length of the choice history. |
private boolean | mCanReadHistoricalDataFlag 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 | mReadShareHistoryCalledFlag 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 | mHistoricalRecordsChangedFlag 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 | mReloadActivitiesFlag whether to reload the activities for the current intent. |
private OnChooseActivityListener | mActivityChoserModelPolicyPolicy for controlling how the model handles chosen activities. |
Methods Summary |
---|
private boolean | addHisoricalRecord(android.widget.ActivityChooserModel$HistoricalRecord historicalRecord)Adds a historical record.
final boolean added = mHistoricalRecords.add(historicalRecord);
if (added) {
mHistoricalRecordsChanged = true;
pruneExcessiveHistoricalRecordsIfNeeded();
persistHistoricalDataIfNeeded();
sortActivitiesIfNeeded();
notifyChanged();
}
return added;
|
public android.content.Intent | chooseActivity(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.
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 void | ensureConsistentState()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 void | finalize()
super.finalize();
mPackageMonitor.unregister();
|
public static android.widget.ActivityChooserModel | get(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.
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.ResolveInfo | getActivity(int index)Gets an activity at a given index.
synchronized (mInstanceLock) {
ensureConsistentState();
return mActivities.get(index).resolveInfo;
}
|
public int | getActivityCount()Gets the number of activities that can handle the intent.
synchronized (mInstanceLock) {
ensureConsistentState();
return mActivities.size();
}
|
public int | getActivityIndex(android.content.pm.ResolveInfo activity)Gets the index of a the given activity.
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.ResolveInfo | getDefaultActivity()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.
synchronized (mInstanceLock) {
ensureConsistentState();
if (!mActivities.isEmpty()) {
return mActivities.get(0).resolveInfo;
}
}
return null;
|
public int | getHistoryMaxSize()Gets the history max size.
synchronized (mInstanceLock) {
return mHistoryMaxSize;
}
|
public int | getHistorySize()Gets the history size.
synchronized (mInstanceLock) {
ensureConsistentState();
return mHistoricalRecords.size();
}
|
public android.content.Intent | getIntent()Gets the intent for which a activity is being chosen.
synchronized (mInstanceLock) {
return mIntent;
}
|
private boolean | loadActivitiesIfNeeded()Loads the activities for the current intent if needed which is
if they are not already loaded for the current intent.
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 void | persistHistoricalDataIfNeeded()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.
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 void | pruneExcessiveHistoricalRecordsIfNeeded()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 boolean | readHistoricalDataIfNeeded()Reads the historical data if necessary which is it has
changed, there is a history file, and there is not persist
in progress.
if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
!TextUtils.isEmpty(mHistoryFileName)) {
mCanReadHistoricalData = false;
mReadShareHistoryCalled = true;
readHistoricalDataImpl();
return true;
}
return false;
|
private void | readHistoricalDataImpl()
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 void | setActivitySorter(android.widget.ActivityChooserModel$ActivitySorter activitySorter)Sets the sorter for ordering activities based on historical data and an intent.
synchronized (mInstanceLock) {
if (mActivitySorter == activitySorter) {
return;
}
mActivitySorter = activitySorter;
if (sortActivitiesIfNeeded()) {
notifyChanged();
}
}
|
public void | setDefaultActivity(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.
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 void | setHistoryMaxSize(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.
synchronized (mInstanceLock) {
if (mHistoryMaxSize == historyMaxSize) {
return;
}
mHistoryMaxSize = historyMaxSize;
pruneExcessiveHistoricalRecordsIfNeeded();
if (sortActivitiesIfNeeded()) {
notifyChanged();
}
}
|
public void | setIntent(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.
synchronized (mInstanceLock) {
if (mIntent == intent) {
return;
}
mIntent = intent;
mReloadActivities = true;
ensureConsistentState();
}
|
public void | setOnChooseActivityListener(android.widget.ActivityChooserModel$OnChooseActivityListener listener)Sets the listener for choosing an activity.
synchronized (mInstanceLock) {
mActivityChoserModelPolicy = listener;
}
|
private boolean | sortActivitiesIfNeeded()Sorts the activities if necessary which is if there is a
sorter, there are some activities to sort, and there is some
historical data.
if (mActivitySorter != null && mIntent != null
&& !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
mActivitySorter.sort(mIntent, mActivities,
Collections.unmodifiableList(mHistoricalRecords));
return true;
}
return false;
|