package com.actionbarsherlock.widget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.database.DataSetObservable;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
}
public void sort(Intent intent, List<ActivityResolveInfo> activities,
List<HistoricalRecord> historicalRecords);
}
}
private static final boolean DEBUG = false;
private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
private static final String TAG_HISTORICAL_RECORDS = "historical-records";
private static final String TAG_HISTORICAL_RECORD = "historical-record";
private static final String ATTRIBUTE_ACTIVITY = "activity";
private static final String ATTRIBUTE_TIME = "time";
private static final String ATTRIBUTE_WEIGHT = "weight";
public static final String DEFAULT_HISTORY_FILE_NAME =
"activity_choser_model_history.xml";
public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
private static final int DEFAULT_ACTIVITY_INFLATION = 5;
private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
private static final String HISTORY_FILE_EXTENSION = ".xml";
private static final int INVALID_INDEX = -1;
private static final Object sRegistryLock = new Object();
private static final Map<String, ActivityChooserModel> sDataModelRegistry =
new HashMap<String, ActivityChooserModel>();
private final Object mInstanceLock = new Object();
private final List<ActivityResolveInfo> mActivites = new ArrayList<ActivityResolveInfo>();
private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
private final Context mContext;
private final String mHistoryFileName;
private Intent mIntent;
private ActivitySorter mActivitySorter = new DefaultSorter();
private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
private boolean mCanReadHistoricalData = true;
private boolean mReadShareHistoryCalled = false;
private boolean mHistoricalRecordsChanged = true;
private final Handler mHandler = new Handler();
private OnChooseActivityListener mActivityChoserModelPolicy;
public static ActivityChooserModel
get(Context context, String historyFileName) {
synchronized (sRegistryLock) {
ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
if (dataModel == null) {
dataModel = new ActivityChooserModel(context, historyFileName);
sDataModelRegistry.put(historyFileName, dataModel);
}
dataModel.readHistoricalData();
return dataModel;
}
}
mContext = context.getApplicationContext();
if (!TextUtils.isEmpty(historyFileName)
&& !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
} else {
mHistoryFileName = historyFileName;
}
}
synchronized (mInstanceLock) {
if (mIntent == intent) {
return;
}
mIntent = intent;
loadActivitiesLocked();
}
}
synchronized (mInstanceLock) {
return mIntent;
}
}
synchronized (mInstanceLock) {
return mActivites.size();
}
}
synchronized (mInstanceLock) {
return mActivites.get(index).resolveInfo;
}
}
List<ActivityResolveInfo> activities = mActivites;
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;
}
ActivityResolveInfo chosenActivity = mActivites.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) {
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;
}
mActivityChoserModelPolicy = listener;
}
synchronized (mInstanceLock) {
if (!mActivites.isEmpty()) {
return mActivites.get(0).resolveInfo;
}
}
return null;
}
ActivityResolveInfo newDefaultActivity = mActivites.get(index);
ActivityResolveInfo oldDefaultActivity = mActivites.get(0);
final float weight;
if (oldDefaultActivity != null) {
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);
}
synchronized (mInstanceLock) {
if (!mCanReadHistoricalData || !mHistoricalRecordsChanged) {
return;
}
mCanReadHistoricalData = false;
mReadShareHistoryCalled = true;
if (!TextUtils.isEmpty(mHistoryFileName)) {
SERIAL_EXECUTOR.execute(new HistoryLoader());
}
}
}
private static final Executor SERIAL_EXECUTOR = Executors.newSingleThreadExecutor();
synchronized (mInstanceLock) {
if (!mReadShareHistoryCalled) {
throw new IllegalStateException("No preceding call to #readHistoricalData");
}
if (!mHistoricalRecordsChanged) {
return;
}
mHistoricalRecordsChanged = false;
mCanReadHistoricalData = true;
if (!TextUtils.isEmpty(mHistoryFileName)) {
SERIAL_EXECUTOR.execute(new HistoryPersister());
}
}
}
synchronized (mInstanceLock) {
if (mActivitySorter == activitySorter) {
return;
}
mActivitySorter = activitySorter;
sortActivities();
}
}
synchronized (mInstanceLock) {
if (mActivitySorter != null && !mActivites.isEmpty()) {
mActivitySorter.sort(mIntent, mActivites,
Collections.unmodifiableList(mHistoricalRecords));
notifyChanged();
}
}
}
synchronized (mInstanceLock) {
if (mHistoryMaxSize == historyMaxSize) {
return;
}
mHistoryMaxSize = historyMaxSize;
pruneExcessiveHistoricalRecordsLocked();
sortActivities();
}
}
synchronized (mInstanceLock) {
return mHistoryMaxSize;
}
}
synchronized (mInstanceLock) {
return mHistoricalRecords.size();
}
}
synchronized (mInstanceLock) {
final boolean added = mHistoricalRecords.add(historicalRecord);
if (added) {
mHistoricalRecordsChanged = true;
pruneExcessiveHistoricalRecordsLocked();
persistHistoricalData();
sortActivities();
}
return added;
}
}
List<HistoricalRecord> choiceRecords = mHistoricalRecords;
final int pruneCount = choiceRecords.size() - mHistoryMaxSize;
if (pruneCount <= 0) {
return;
}
mHistoricalRecordsChanged = true;
for (int i = 0; i < pruneCount; i++) {
HistoricalRecord prunedRecord = choiceRecords.remove(0);
if (DEBUG) {
Log.i(LOG_TAG, "Pruned: " + prunedRecord);
}
}
}
mActivites.clear();
if (mIntent != null) {
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);
mActivites.add(new ActivityResolveInfo(resolveInfo));
}
sortActivities();
} else {
notifyChanged();
}
}
public final ComponentName activity;
public final long time;
public final float weight;
this(ComponentName.unflattenFromString(activityName), time, weight);
}
this.activity = activityName;
this.time = time;
this.weight = weight;
}
@Override
final int prime = 31;
int result = 1;
result = prime * result + ((activity == null) ? 0 : activity.hashCode());
result = prime * result + (int) (time ^ (time >>> 32));
result = prime * result + Float.floatToIntBits(weight);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
HistoricalRecord other = (HistoricalRecord) obj;
if (activity == null) {
if (other.activity != null) {
return false;
}
} else if (!activity.equals(other.activity)) {
return false;
}
if (time != other.time) {
return false;
}
if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
return false;
}
return true;
}
@Override
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append("; activity:").append(activity);
builder.append("; time:").append(time);
builder.append("; weight:").append(new BigDecimal(weight));
builder.append("]");
return builder.toString();
}
}
public final ResolveInfo resolveInfo;
public float weight;
this.resolveInfo = resolveInfo;
}
@Override
return 31 + Float.floatToIntBits(weight);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ActivityResolveInfo other = (ActivityResolveInfo) obj;
if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
return false;
}
return true;
}
public int compareTo(ActivityResolveInfo another) {
return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
}
@Override
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append("resolveInfo:").append(resolveInfo.toString());
builder.append("; weight:").append(new BigDecimal(weight));
builder.append("]");
return builder.toString();
}
}
private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap =
new HashMap<String, ActivityResolveInfo>();
public void sort(Intent intent, List<ActivityResolveInfo> activities,
List<HistoricalRecord> historicalRecords) {
Map<String, ActivityResolveInfo> packageNameToActivityMap =
mPackageNameToActivityMap;
packageNameToActivityMap.clear();
final int activityCount = activities.size();
for (int i = 0; i < activityCount; i++) {
ActivityResolveInfo activity = activities.get(i);
activity.weight = 0.0f;
String packageName = activity.resolveInfo.activityInfo.packageName;
packageNameToActivityMap.put(packageName, activity);
}
final int lastShareIndex = historicalRecords.size() - 1;
float nextRecordWeight = 1;
for (int i = lastShareIndex; i >= 0; i--) {
HistoricalRecord historicalRecord = historicalRecords.get(i);
String packageName = historicalRecord.activity.getPackageName();
ActivityResolveInfo activity = packageNameToActivityMap.get(packageName);
if (activity != null) {
activity.weight += historicalRecord.weight * nextRecordWeight;
nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
}
}
Collections.sort(activities);
if (DEBUG) {
for (int i = 0; i < activityCount; i++) {
Log.i(LOG_TAG, "Sorted: " + activities.get(i));
}
}
}
}
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> readRecords = new ArrayList<HistoricalRecord>();
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);
readRecords.add(readRecord);
if (DEBUG) {
Log.i(LOG_TAG, "Read " + readRecord.toString());
}
}
if (DEBUG) {
Log.i(LOG_TAG, "Read " + readRecords.size() + " historical records.");
}
synchronized (mInstanceLock) {
Set<HistoricalRecord> uniqueShareRecords =
new LinkedHashSet<HistoricalRecord>(readRecords);
List<HistoricalRecord> historicalRecords = mHistoricalRecords;
final int historicalRecordsCount = historicalRecords.size();
for (int i = historicalRecordsCount - 1; i >= 0; i--) {
HistoricalRecord historicalRecord = historicalRecords.get(i);
uniqueShareRecords.add(historicalRecord);
}
if (historicalRecords.size() == uniqueShareRecords.size()) {
return;
}
historicalRecords.clear();
historicalRecords.addAll(uniqueShareRecords);
mHistoricalRecordsChanged = true;
mHandler.post(new Runnable() {
pruneExcessiveHistoricalRecordsLocked();
sortActivities();
}
});
}
} 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) {
}
}
}
}
}
FileOutputStream fos = null;
List<HistoricalRecord> records = null;
synchronized (mInstanceLock) {
records = new ArrayList<HistoricalRecord>(mHistoricalRecords);
}
try {
fos = mContext.openFileOutput(mHistoryFileName, Context.MODE_PRIVATE);
} catch (FileNotFoundException fnfe) {
Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, fnfe);
return;
}
XmlSerializer serializer = Xml.newSerializer();
try {
serializer.setOutput(fos, null);
serializer.startDocument("UTF-8", true);
serializer.startTag(null, TAG_HISTORICAL_RECORDS);
final int recordCount = records.size();
for (int i = 0; i < recordCount; i++) {
HistoricalRecord record = records.remove(0);
serializer.startTag(null, TAG_HISTORICAL_RECORD);
serializer.attribute(null, ATTRIBUTE_ACTIVITY, record.activity.flattenToString());
serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
serializer.endTag(null, TAG_HISTORICAL_RECORD);
if (DEBUG) {
Log.i(LOG_TAG, "Wrote " + record.toString());
}
}
serializer.endTag(null, TAG_HISTORICAL_RECORDS);
serializer.endDocument();
if (DEBUG) {
Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
}
} catch (IllegalArgumentException iae) {
Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
} catch (IllegalStateException ise) {
Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
} catch (IOException ioe) {
Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
}
}
}
}
}
}