mirror of https://github.com/tasks/tasks
Re-add submodules as normal files
parent
374784c560
commit
0e1b749aee
@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright (c) 2009, Todoroo Inc
|
||||
* All Rights Reserved
|
||||
* http://www.todoroo.com
|
||||
*/
|
||||
package com.todoroo.andlib.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.database.sqlite.SQLiteDatabase.CursorFactory;
|
||||
import android.util.Log;
|
||||
|
||||
import com.todoroo.andlib.data.Property.PropertyVisitor;
|
||||
import com.todoroo.andlib.service.ContextManager;
|
||||
|
||||
/**
|
||||
* AbstractDatabase is a database abstraction which wraps a SQLite database.
|
||||
* <p>
|
||||
* Users of this class are in charge of the database's lifecycle - ensuring that
|
||||
* the database is open when needed and closed when usage is finished. Within an
|
||||
* activity, this is typically accomplished through the onResume and onPause
|
||||
* methods, though if the database is not needed for the activity's entire
|
||||
* lifecycle, it can be closed earlier.
|
||||
* <p>
|
||||
* Direct querying is not recommended for type safety reasons. Instead, use one
|
||||
* of the service classes to issue the request and return a {@link TodorooCursor}.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
abstract public class AbstractDatabase {
|
||||
|
||||
// --- abstract methods
|
||||
|
||||
/**
|
||||
* @return database name
|
||||
*/
|
||||
protected abstract String getName();
|
||||
|
||||
/**
|
||||
* @return all tables in this database
|
||||
*/
|
||||
protected abstract Table[] getTables();
|
||||
|
||||
/**
|
||||
* @return database version
|
||||
*/
|
||||
protected abstract int getVersion();
|
||||
|
||||
/**
|
||||
* Called after database and tables are created. Use this method to
|
||||
* create indices and perform other database maintenance
|
||||
*/
|
||||
protected abstract void onCreateTables();
|
||||
|
||||
/**
|
||||
* Upgrades an open database from one version to the next
|
||||
* @param oldVersion
|
||||
* @param newVersion
|
||||
* @return true if upgrade was handled, false otherwise
|
||||
*/
|
||||
protected abstract boolean onUpgrade(int oldVersion, int newVersion);
|
||||
|
||||
// --- protected variables
|
||||
|
||||
/**
|
||||
* SQLiteOpenHelper that takes care of database operations
|
||||
*/
|
||||
protected SQLiteOpenHelper helper = null;
|
||||
|
||||
/**
|
||||
* Internal pointer to open database. Hides the fact that there is a
|
||||
* database and a wrapper by making a single monolithic interface
|
||||
*/
|
||||
protected SQLiteDatabase database = null;
|
||||
|
||||
// --- internal implementation
|
||||
|
||||
/**
|
||||
* Return the name of the table containing these models
|
||||
* @param modelType
|
||||
* @return
|
||||
*/
|
||||
public final Table getTable(Class<? extends AbstractModel> modelType) {
|
||||
for(Table table : getTables()) {
|
||||
if(table.modelClass.equals(modelType))
|
||||
return table;
|
||||
}
|
||||
throw new UnsupportedOperationException("Unknown model class " + modelType); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
protected final void initializeHelper() {
|
||||
if(helper == null)
|
||||
helper = new DatabaseHelper(ContextManager.getContext(),
|
||||
getName(), null, getVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the database for writing. Must be closed afterwards. If user is
|
||||
* out of disk space, database may be opened for reading instead
|
||||
*/
|
||||
public synchronized final void openForWriting() {
|
||||
initializeHelper();
|
||||
|
||||
try {
|
||||
database = helper.getWritableDatabase();
|
||||
} catch (SQLiteException writeException) {
|
||||
Log.e("database-" + getName(), "Error opening db",
|
||||
writeException);
|
||||
try {
|
||||
// provide read-only database
|
||||
openForReading();
|
||||
} catch (SQLiteException readException) {
|
||||
// throw original write exception
|
||||
throw writeException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the database for reading. Must be closed afterwards
|
||||
*/
|
||||
public synchronized final void openForReading() {
|
||||
initializeHelper();
|
||||
database = helper.getReadableDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database if it has been opened previously
|
||||
*/
|
||||
public synchronized final void close() {
|
||||
if(database != null)
|
||||
database.close();
|
||||
database = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data in database. Warning: this does what it says. Any open
|
||||
* database resources will be abruptly closed.
|
||||
*/
|
||||
public synchronized final void clear() {
|
||||
close();
|
||||
ContextManager.getContext().deleteDatabase(getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return sql database. throws error if database was not opened
|
||||
*/
|
||||
public final SQLiteDatabase getDatabase() {
|
||||
if(database == null)
|
||||
throw new IllegalStateException("Database was not opened!");
|
||||
return database;
|
||||
}
|
||||
|
||||
// --- helper classes
|
||||
|
||||
/**
|
||||
* Default implementation of Astrid database helper
|
||||
*/
|
||||
private class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public DatabaseHelper(Context context, String name,
|
||||
CursorFactory factory, int version) {
|
||||
super(context, name, factory, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to create the database tables
|
||||
*/
|
||||
@Override
|
||||
public synchronized void onCreate(SQLiteDatabase db) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
SqlConstructorVisitor sqlVisitor = new SqlConstructorVisitor();
|
||||
|
||||
// create tables
|
||||
for(Table table : getTables()) {
|
||||
sql.append("CREATE TABLE IF NOT EXISTS ").append(table.name).append('(').
|
||||
append(AbstractModel.ID_PROPERTY).append(" INTEGER PRIMARY KEY AUTOINCREMENT");
|
||||
for(Property<?> property : table.getProperties()) {
|
||||
if(AbstractModel.ID_PROPERTY.name.equals(property.name))
|
||||
continue;
|
||||
sql.append(',').append(property.accept(sqlVisitor, null));
|
||||
}
|
||||
sql.append(')');
|
||||
db.execSQL(sql.toString());
|
||||
sql.setLength(0);
|
||||
}
|
||||
|
||||
// post-table-creation
|
||||
database = db;
|
||||
onCreateTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to upgrade the database to a new version
|
||||
*/
|
||||
@Override
|
||||
public synchronized void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
Log.w("database-" + getName(), String.format("Upgrading database from version %d to %d.",
|
||||
oldVersion, newVersion));
|
||||
|
||||
database = db;
|
||||
if(!AbstractDatabase.this.onUpgrade(oldVersion, newVersion)) {
|
||||
// We don't know how to handle this case because someone forgot to
|
||||
// implement the upgrade. We can't drop tables, we can only
|
||||
// throw a nasty exception at this time
|
||||
|
||||
throw new IllegalStateException("Missing database migration " +
|
||||
"from " + oldVersion + " to " + newVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that returns SQL constructor for this property
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class SqlConstructorVisitor implements PropertyVisitor<String, Void> {
|
||||
|
||||
public String visitDouble(Property<Double> property, Void data) {
|
||||
return String.format("%s REAL", property.name);
|
||||
}
|
||||
|
||||
public String visitInteger(Property<Integer> property, Void data) {
|
||||
return String.format("%s INTEGER", property.name);
|
||||
}
|
||||
|
||||
public String visitLong(Property<Long> property, Void data) {
|
||||
return String.format("%s INTEGER", property.name);
|
||||
}
|
||||
|
||||
public String visitString(Property<String> property, Void data) {
|
||||
return String.format("%s TEXT", property.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,354 @@
|
||||
/*
|
||||
* Copyright (c) 2009, Todoroo Inc
|
||||
* All Rights Reserved
|
||||
* http://www.todoroo.com
|
||||
*/
|
||||
package com.todoroo.andlib.data;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.todoroo.andlib.data.Property.IntegerProperty;
|
||||
import com.todoroo.andlib.data.Property.LongProperty;
|
||||
import com.todoroo.andlib.data.Property.PropertyVisitor;
|
||||
|
||||
/**
|
||||
* <code>AbstractModel</code> represents a row in a database.
|
||||
* <p>
|
||||
* A single database can be represented by multiple <code>AbstractModel</code>s
|
||||
* corresponding to different queries that return a different set of columns.
|
||||
* Each model exposes a set of properties that it contains.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractModel implements Parcelable {
|
||||
|
||||
// --- static variables
|
||||
|
||||
private static final ContentValuesSavingVisitor saver = new ContentValuesSavingVisitor();
|
||||
|
||||
// --- constants
|
||||
|
||||
/** id property common to all models */
|
||||
protected static final String ID_PROPERTY_NAME = "_id"; //$NON-NLS-1$
|
||||
|
||||
/** id field common to all models */
|
||||
public static final IntegerProperty ID_PROPERTY = new IntegerProperty(null, ID_PROPERTY_NAME);
|
||||
|
||||
/** sentinel for objects without an id */
|
||||
public static final long NO_ID = 0;
|
||||
|
||||
// --- abstract methods
|
||||
|
||||
/** Get the default values for this object */
|
||||
abstract public ContentValues getDefaultValues();
|
||||
|
||||
// --- data store variables and management
|
||||
|
||||
/* Data Source Ordering:
|
||||
*
|
||||
* In order to return the best data, we want to check first what the user
|
||||
* has explicitly set (setValues), then the values we have read out of
|
||||
* the database (values), then defaults (getDefaultValues)
|
||||
*/
|
||||
|
||||
/** User set values */
|
||||
protected ContentValues setValues = null;
|
||||
|
||||
/** Values from database */
|
||||
protected ContentValues values = null;
|
||||
|
||||
/** Get database-read values for this object */
|
||||
public ContentValues getDatabaseValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
/** Get the user-set values for this object */
|
||||
public ContentValues getSetValues() {
|
||||
return setValues;
|
||||
}
|
||||
|
||||
/** Get a list of all field/value pairs merged across data sources */
|
||||
public ContentValues getMergedValues() {
|
||||
ContentValues mergedValues = new ContentValues();
|
||||
|
||||
ContentValues defaultValues = getDefaultValues();
|
||||
if(defaultValues != null)
|
||||
mergedValues.putAll(defaultValues);
|
||||
if(values != null)
|
||||
mergedValues.putAll(values);
|
||||
if(setValues != null)
|
||||
mergedValues.putAll(setValues);
|
||||
|
||||
return mergedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data on this model
|
||||
*/
|
||||
public void clear() {
|
||||
values = null;
|
||||
setValues = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use merged values to compare two models to each other. Must be of
|
||||
* exactly the same class.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if(other == null || other.getClass() != getClass())
|
||||
return false;
|
||||
|
||||
return getMergedValues().equals(((AbstractModel)other).getMergedValues());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getMergedValues().hashCode() ^ getClass().hashCode();
|
||||
}
|
||||
|
||||
// --- data retrieval
|
||||
|
||||
/**
|
||||
* Reads all properties from the supplied cursor and store
|
||||
*/
|
||||
protected synchronized void readPropertiesFromCursor(TodorooCursor<? extends AbstractModel> cursor) {
|
||||
if (values == null)
|
||||
values = new ContentValues();
|
||||
|
||||
// clears user-set values
|
||||
setValues = null;
|
||||
|
||||
for (Property<?> property : cursor.getProperties()) {
|
||||
saver.save(property, values, cursor.get(property));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the given property. Make sure this model has this property!
|
||||
*/
|
||||
public <TYPE> TYPE getValue(Property<TYPE> property) {
|
||||
if(setValues != null && setValues.containsKey(property.name))
|
||||
return (TYPE)setValues.get(property.name);
|
||||
|
||||
if(values != null && values.containsKey(property.name))
|
||||
return (TYPE)values.get(property.name);
|
||||
|
||||
if(getDefaultValues().containsKey(property.name))
|
||||
return (TYPE)getDefaultValues().get(property.name);
|
||||
|
||||
throw new UnsupportedOperationException(
|
||||
"Model Error: Did not read property " + property.name); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to get the identifier of the model, if it exists.
|
||||
* Returns 0
|
||||
*/
|
||||
abstract public long getId();
|
||||
|
||||
protected long getIdHelper(LongProperty id) {
|
||||
if(setValues != null && setValues.containsKey(id.name))
|
||||
return setValues.getAsLong(id.name);
|
||||
else if(values != null && values.containsKey(id.name))
|
||||
return values.getAsLong(id.name);
|
||||
else
|
||||
return NO_ID;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
if (setValues == null)
|
||||
setValues = new ContentValues();
|
||||
|
||||
if(id == NO_ID)
|
||||
setValues.remove(ID_PROPERTY_NAME);
|
||||
else
|
||||
setValues.put(ID_PROPERTY_NAME, id);
|
||||
}
|
||||
|
||||
// --- data storage
|
||||
|
||||
/**
|
||||
* Check whether the user has changed this property value and it should be
|
||||
* stored for saving in the database
|
||||
*/
|
||||
protected synchronized <TYPE> boolean shouldSaveValue(
|
||||
Property<TYPE> property, TYPE newValue) {
|
||||
|
||||
// we've already decided to save it, so overwrite old value
|
||||
if (setValues.containsKey(property.name))
|
||||
return true;
|
||||
|
||||
// values contains this key, we should check it out
|
||||
if(values != null && values.containsKey(property.name)) {
|
||||
TYPE value = getValue(property);
|
||||
if (value == null) {
|
||||
if (newValue == null)
|
||||
return false;
|
||||
} else if (value.equals(newValue))
|
||||
return false;
|
||||
}
|
||||
|
||||
// otherwise, good to save
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given property. Make sure this model has this property!
|
||||
*/
|
||||
public synchronized <TYPE> void setValue(Property<TYPE> property,
|
||||
TYPE value) {
|
||||
if (setValues == null)
|
||||
setValues = new ContentValues();
|
||||
if (!shouldSaveValue(property, value))
|
||||
return;
|
||||
|
||||
saver.save(property, setValues, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the key for the given property
|
||||
* @param property
|
||||
*/
|
||||
public synchronized void clearValue(Property<?> property) {
|
||||
if(setValues != null)
|
||||
setValues.remove(property.name);
|
||||
}
|
||||
|
||||
// --- property management
|
||||
|
||||
/**
|
||||
* Looks inside the given class and finds all declared properties
|
||||
*/
|
||||
protected static Property<?>[] generateProperties(Class<? extends AbstractModel> cls) {
|
||||
ArrayList<Property<?>> properties = new ArrayList<Property<?>>();
|
||||
if(cls.getSuperclass() != AbstractModel.class)
|
||||
properties.addAll(Arrays.asList(generateProperties(
|
||||
(Class<? extends AbstractModel>) cls.getSuperclass())));
|
||||
|
||||
// a property is public, static & extends Property
|
||||
for(Field field : cls.getFields()) {
|
||||
if((field.getModifiers() & Modifier.STATIC) == 0)
|
||||
continue;
|
||||
if(!Property.class.isAssignableFrom(field.getType()))
|
||||
continue;
|
||||
try {
|
||||
properties.add((Property<?>) field.get(null));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return properties.toArray(new Property<?>[properties.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that saves a value into a content values store
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class ContentValuesSavingVisitor implements PropertyVisitor<Void, Object> {
|
||||
|
||||
private ContentValues store;
|
||||
|
||||
public synchronized void save(Property<?> property, ContentValues newStore, Object value) {
|
||||
this.store = newStore;
|
||||
property.accept(this, value);
|
||||
}
|
||||
|
||||
public Void visitDouble(Property<Double> property, Object value) {
|
||||
store.put(property.name, (Double) value);
|
||||
return null;
|
||||
}
|
||||
|
||||
public Void visitInteger(Property<Integer> property, Object value) {
|
||||
store.put(property.name, (Integer) value);
|
||||
return null;
|
||||
}
|
||||
|
||||
public Void visitLong(Property<Long> property, Object value) {
|
||||
store.put(property.name, (Long) value);
|
||||
return null;
|
||||
}
|
||||
|
||||
public Void visitString(Property<String> property, Object value) {
|
||||
store.put(property.name, (String) value);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- parcelable helpers
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(setValues, 0);
|
||||
dest.writeParcelable(values, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* In addition to overriding this class, model classes should create
|
||||
* a static final variable named "CREATOR" in order to satisfy the
|
||||
* requirements of the Parcelable interface.
|
||||
*/
|
||||
abstract protected Parcelable.Creator<? extends AbstractModel> getCreator();
|
||||
|
||||
/**
|
||||
* Parcelable creator helper
|
||||
*/
|
||||
protected static final class ModelCreator<TYPE extends AbstractModel>
|
||||
implements Parcelable.Creator<TYPE> {
|
||||
|
||||
private Class<TYPE> cls;
|
||||
|
||||
public ModelCreator(Class<TYPE> cls) {
|
||||
super();
|
||||
this.cls = cls;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public TYPE createFromParcel(Parcel source) {
|
||||
TYPE model;
|
||||
try {
|
||||
model = cls.newInstance();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
model.setValues = source.readParcelable(ContentValues.class.getClassLoader());
|
||||
model.values = source.readParcelable(ContentValues.class.getClassLoader());
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public TYPE[] newArray(int size) {
|
||||
return (TYPE[]) Array.newInstance(cls, size);
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (c) 2009, Todoroo Inc
|
||||
* All Rights Reserved
|
||||
* http://www.todoroo.com
|
||||
*/
|
||||
package com.todoroo.andlib.data;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.todoroo.andlib.data.sql.Criterion;
|
||||
import com.todoroo.andlib.data.sql.Query;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Abstract data access object
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class GenericDao<TYPE extends AbstractModel> {
|
||||
|
||||
private Class<TYPE> modelClass;
|
||||
|
||||
private Table table;
|
||||
|
||||
private AbstractDatabase database;
|
||||
|
||||
public GenericDao(Class<TYPE> modelClass) {
|
||||
this.modelClass = modelClass;
|
||||
}
|
||||
|
||||
public GenericDao(Class<TYPE> modelClass, AbstractDatabase database) {
|
||||
this.modelClass = modelClass;
|
||||
setDatabase(database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a database
|
||||
* @param database
|
||||
*/
|
||||
protected void setDatabase(AbstractDatabase database) {
|
||||
this.database = database;
|
||||
table = database.getTable(modelClass);
|
||||
}
|
||||
|
||||
// --- dao methods
|
||||
|
||||
/**
|
||||
* Construct a query with SQL DSL objects
|
||||
* @param database
|
||||
* @param properties
|
||||
* @param builder
|
||||
* @param where
|
||||
* @param groupBy
|
||||
* @param sortOrder
|
||||
* @return
|
||||
*/
|
||||
public TodorooCursor<TYPE> query(Query query) {
|
||||
query.from(table);
|
||||
Cursor cursor = database.getDatabase().rawQuery(query.toString(), null);
|
||||
return new TodorooCursor<TYPE>(cursor, query.getFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns object corresponding to the given identifier
|
||||
*
|
||||
* @param database
|
||||
* @param table
|
||||
* name of table
|
||||
* @param properties
|
||||
* properties to read
|
||||
* @param id
|
||||
* id of item
|
||||
* @return
|
||||
*/
|
||||
public TYPE fetch(long id, Property<?>... properties) {
|
||||
TodorooCursor<TYPE> cursor = fetchItem(id, properties);
|
||||
try {
|
||||
if (cursor.getCount() == 0)
|
||||
return null;
|
||||
Constructor<TYPE> constructor = modelClass.getConstructor(TodorooCursor.class);
|
||||
return constructor.newInstance(cursor);
|
||||
} catch (SecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given id
|
||||
*
|
||||
* @param database
|
||||
* @param id
|
||||
* @return true if delete was successful
|
||||
*/
|
||||
public boolean delete(long id) {
|
||||
return database.getDatabase().delete(table.name,
|
||||
AbstractModel.ID_PROPERTY.eq(id).toString(), null) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all matching a clause
|
||||
* @param database
|
||||
* @param where
|
||||
* @return # of deleted items
|
||||
*/
|
||||
public int deleteWhere(Criterion where) {
|
||||
return database.getDatabase().delete(table.name,
|
||||
where.toString(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given object to the database. Creates a new object if
|
||||
* model id property has not been set
|
||||
*
|
||||
* @return true on success.
|
||||
*/
|
||||
public boolean persist(AbstractModel item) {
|
||||
if (item.getId() == AbstractModel.NO_ID) {
|
||||
return createItem(item);
|
||||
} else {
|
||||
ContentValues values = item.getSetValues();
|
||||
|
||||
if (values.size() == 0) // nothing changed
|
||||
return true;
|
||||
|
||||
return saveItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the given item.
|
||||
*
|
||||
* @param database
|
||||
* @param table
|
||||
* table name
|
||||
* @param item
|
||||
* item model
|
||||
* @return returns true on success.
|
||||
*/
|
||||
public boolean createItem(AbstractModel item) {
|
||||
long newRow = database.getDatabase().insert(table.name,
|
||||
AbstractModel.ID_PROPERTY.name, item.getMergedValues());
|
||||
item.setId(newRow);
|
||||
return newRow >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given item.
|
||||
*
|
||||
* @param database
|
||||
* @param table
|
||||
* table name
|
||||
* @param item
|
||||
* item model
|
||||
* @return returns true on success.
|
||||
*/
|
||||
public boolean saveItem(AbstractModel item) {
|
||||
ContentValues values = item.getSetValues();
|
||||
if(values.size() == 0) // nothing changed
|
||||
return true;
|
||||
return database.getDatabase().update(table.name, values,
|
||||
AbstractModel.ID_PROPERTY.eq(item.getId()).toString(), null) > 0;
|
||||
}
|
||||
|
||||
// --- helper methods
|
||||
|
||||
|
||||
/**
|
||||
* Returns cursor to object corresponding to the given identifier
|
||||
*
|
||||
* @param database
|
||||
* @param table
|
||||
* name of table
|
||||
* @param properties
|
||||
* properties to read
|
||||
* @param id
|
||||
* id of item
|
||||
* @return
|
||||
*/
|
||||
protected TodorooCursor<TYPE> fetchItem(long id, Property<?>... properties) {
|
||||
TodorooCursor<TYPE> cursor = query(
|
||||
Query.select(properties).where(AbstractModel.ID_PROPERTY.eq(id)));
|
||||
cursor.moveToFirst();
|
||||
return new TodorooCursor<TYPE>(cursor, properties);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright (c) 2009, Todoroo Inc
|
||||
* All Rights Reserved
|
||||
* http://www.todoroo.com
|
||||
*/
|
||||
package com.todoroo.andlib.data;
|
||||
|
||||
import com.todoroo.andlib.data.sql.Field;
|
||||
|
||||
/**
|
||||
* Property represents a typed column in a database.
|
||||
*
|
||||
* Within a given database row, the parameter may not exist, in which case the
|
||||
* value is null, it may be of an incorrect type, in which case an exception is
|
||||
* thrown, or the correct type, in which case the value is returned.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
* @param <TYPE>
|
||||
* a database supported type, such as String or Integer
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public abstract class Property<TYPE> extends Field implements Cloneable {
|
||||
|
||||
// --- implementation
|
||||
|
||||
/** The database table name this property */
|
||||
public final Table table;
|
||||
|
||||
/** The database column name for this property */
|
||||
public final String name;
|
||||
|
||||
/**
|
||||
* Create a property by table and column name. Uses the default property
|
||||
* expression which is derived from default table name
|
||||
*/
|
||||
protected Property(Table table, String columnName) {
|
||||
this(table, columnName, (table == null) ? (columnName) : (table.name + "." + columnName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a property by table and column name, manually specifying an
|
||||
* expression to use in SQL
|
||||
*/
|
||||
protected Property(Table table, String columnName, String expression) {
|
||||
super(expression);
|
||||
this.table = table;
|
||||
this.name = columnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a visitor
|
||||
*/
|
||||
abstract public <RETURN, PARAMETER> RETURN accept(
|
||||
PropertyVisitor<RETURN, PARAMETER> visitor, PARAMETER data);
|
||||
|
||||
/**
|
||||
* Return a clone of this property
|
||||
*/
|
||||
@Override
|
||||
public Property<TYPE> clone() {
|
||||
try {
|
||||
return (Property<TYPE>) super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- helper classes and interfaces
|
||||
|
||||
/**
|
||||
* Visitor interface for property classes
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public interface PropertyVisitor<RETURN, PARAMETER> {
|
||||
public RETURN visitInteger(Property<Integer> property, PARAMETER data);
|
||||
|
||||
public RETURN visitLong(Property<Long> property, PARAMETER data);
|
||||
|
||||
public RETURN visitDouble(Property<Double> property, PARAMETER data);
|
||||
|
||||
public RETURN visitString(Property<String> property, PARAMETER data);
|
||||
}
|
||||
|
||||
// --- children
|
||||
|
||||
/**
|
||||
* Integer property type. See {@link Property}
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class IntegerProperty extends Property<Integer> {
|
||||
|
||||
public IntegerProperty(Table table, String name) {
|
||||
super(table, name);
|
||||
}
|
||||
|
||||
protected IntegerProperty(Table table, String name, String expression) {
|
||||
super(table, name, expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <RETURN, PARAMETER> RETURN accept(
|
||||
PropertyVisitor<RETURN, PARAMETER> visitor, PARAMETER data) {
|
||||
return visitor.visitInteger(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String property type. See {@link Property}
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class StringProperty extends Property<String> {
|
||||
|
||||
public StringProperty(Table table, String name) {
|
||||
super(table, name);
|
||||
}
|
||||
|
||||
protected StringProperty(Table table, String name, String expression) {
|
||||
super(table, name, expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <RETURN, PARAMETER> RETURN accept(
|
||||
PropertyVisitor<RETURN, PARAMETER> visitor, PARAMETER data) {
|
||||
return visitor.visitString(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Double property type. See {@link Property}
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class DoubleProperty extends Property<Double> {
|
||||
|
||||
public DoubleProperty(Table table, String name) {
|
||||
super(table, name);
|
||||
}
|
||||
|
||||
protected DoubleProperty(Table table, String name, String expression) {
|
||||
super(table, name, expression);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <RETURN, PARAMETER> RETURN accept(
|
||||
PropertyVisitor<RETURN, PARAMETER> visitor, PARAMETER data) {
|
||||
return visitor.visitDouble(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Long property type. See {@link Property}
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class LongProperty extends Property<Long> {
|
||||
|
||||
public LongProperty(Table table, String name) {
|
||||
super(table, name);
|
||||
}
|
||||
|
||||
protected LongProperty(Table table, String name, String expression) {
|
||||
super(table, name, expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <RETURN, PARAMETER> RETURN accept(
|
||||
PropertyVisitor<RETURN, PARAMETER> visitor, PARAMETER data) {
|
||||
return visitor.visitLong(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
// --- pseudo-properties
|
||||
|
||||
/** Runs a SQL function and returns the result as a string */
|
||||
public static class StringFunctionProperty extends StringProperty {
|
||||
public StringFunctionProperty(String function, String columnName) {
|
||||
super(null, columnName, function + " AS " + columnName);
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs a SQL function and returns the result as a string */
|
||||
public static class IntegerFunctionProperty extends IntegerProperty {
|
||||
public IntegerFunctionProperty(String function, String columnName) {
|
||||
super(null, columnName, function + " AS " + columnName);
|
||||
}
|
||||
}
|
||||
|
||||
/** Counting in aggregated tables. Returns the result of COUNT(1) */
|
||||
public static final class CountProperty extends IntegerFunctionProperty {
|
||||
public CountProperty() {
|
||||
super("COUNT(1)", "count");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.todoroo.andlib.data;
|
||||
|
||||
import com.todoroo.andlib.data.sql.Field;
|
||||
|
||||
/**
|
||||
* Table class. Most fields are final, so methods such as <code>as</code> will
|
||||
* clone the table when it returns.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public final class Table extends com.todoroo.andlib.data.sql.Table {
|
||||
public final String name;
|
||||
public final Class<? extends AbstractModel> modelClass;
|
||||
|
||||
public Table(String name, Class<? extends AbstractModel> modelClass) {
|
||||
this(name, modelClass, null);
|
||||
}
|
||||
|
||||
public Table(String name, Class<? extends AbstractModel> modelClass, String alias) {
|
||||
super(name);
|
||||
this.name = name;
|
||||
this.alias = alias;
|
||||
this.modelClass = modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a list of properties from model class by reflection
|
||||
* @return property array
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public Property<?>[] getProperties() {
|
||||
try {
|
||||
return (Property<?>[])modelClass.getField("PROPERTIES").get(null);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- for sql-dsl
|
||||
|
||||
/**
|
||||
* Create a new join table based on this table, but with an alias
|
||||
*/
|
||||
@Override
|
||||
public Table as(String newAlias) {
|
||||
return new Table(name, modelClass, newAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a field object based on the given property
|
||||
* @param property
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public Field field(Property<?> property) {
|
||||
if(alias != null)
|
||||
return Field.field(alias + "." + property.name);
|
||||
return Field.field(name + "." + property.name);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.andlib.data;
|
||||
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
|
||||
import com.todoroo.andlib.data.Property.PropertyVisitor;
|
||||
|
||||
/**
|
||||
* AstridCursor wraps a cursor and allows users to query for individual
|
||||
* {@link Property} types or read an entire {@link AbstractModel} from
|
||||
* a database row.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
* @param <TYPE> a model type that is returned by this cursor
|
||||
*/
|
||||
public class TodorooCursor<TYPE extends AbstractModel> extends CursorWrapper {
|
||||
|
||||
/** Properties read by this cursor */
|
||||
private final Property<?>[] properties;
|
||||
|
||||
/** Weakly cache field name to column id references for this cursor.
|
||||
* Because it's a weak hash map, entire keys can be discarded by GC */
|
||||
private final WeakHashMap<String, Integer> columnIndexCache;
|
||||
|
||||
/** Property reading visitor */
|
||||
private static final CursorReadingVisitor reader = new CursorReadingVisitor();
|
||||
|
||||
/**
|
||||
* Create an <code>AstridCursor</code> from the supplied {@link Cursor}
|
||||
* object.
|
||||
*
|
||||
* @param cursor
|
||||
* @param properties properties read from this cursor
|
||||
*/
|
||||
public TodorooCursor(Cursor cursor, Property<?>[] properties) {
|
||||
super(cursor);
|
||||
|
||||
this.properties = properties;
|
||||
columnIndexCache = new WeakHashMap<String, Integer>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for the given property on the underlying {@link Cursor}
|
||||
*
|
||||
* @param <PROPERTY_TYPE> type to return
|
||||
* @param property to retrieve
|
||||
* @return
|
||||
*/
|
||||
public <PROPERTY_TYPE> PROPERTY_TYPE get(Property<PROPERTY_TYPE> property) {
|
||||
return (PROPERTY_TYPE)property.accept(reader, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets entire property list
|
||||
* @return
|
||||
*/
|
||||
public Property<?>[] getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use cache to get the column index for the given field name
|
||||
*/
|
||||
public synchronized int getColumnIndexFromCache(String field) {
|
||||
Integer index = columnIndexCache.get(field);
|
||||
if(index == null) {
|
||||
index = getColumnIndexOrThrow(field);
|
||||
columnIndexCache.put(field, index);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that reads the given property from a cursor
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class CursorReadingVisitor implements PropertyVisitor<Object, TodorooCursor<?>> {
|
||||
|
||||
public Object visitDouble(Property<Double> property,
|
||||
TodorooCursor<?> cursor) {
|
||||
return cursor.getDouble(cursor.getColumnIndexFromCache(property.name));
|
||||
}
|
||||
|
||||
public Object visitInteger(Property<Integer> property,
|
||||
TodorooCursor<?> cursor) {
|
||||
return cursor.getInt(cursor.getColumnIndexFromCache(property.name));
|
||||
}
|
||||
|
||||
public Object visitLong(Property<Long> property, TodorooCursor<?> cursor) {
|
||||
return cursor.getLong(cursor.getColumnIndexFromCache(property.name));
|
||||
}
|
||||
|
||||
public Object visitString(Property<String> property,
|
||||
TodorooCursor<?> cursor) {
|
||||
return cursor.getString(cursor.getColumnIndexFromCache(property.name));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
public class Constants {
|
||||
static final String SELECT = "SELECT";
|
||||
static final String SPACE = " ";
|
||||
static final String AS = "AS";
|
||||
static final String COMMA = ",";
|
||||
static final String FROM = "FROM";
|
||||
static final String ON = "ON";
|
||||
static final String JOIN = "JOIN";
|
||||
static final String ALL = "*";
|
||||
static final String LEFT_PARENTHESIS = "(";
|
||||
static final String RIGHT_PARENTHESIS = ")";
|
||||
static final String AND = "AND";
|
||||
static final String BETWEEN = "BETWEEN";
|
||||
static final String LIKE = "LIKE";
|
||||
static final String OR = "OR";
|
||||
static final String ORDER_BY = "ORDER BY";
|
||||
static final String GROUP_BY = "GROUP BY";
|
||||
static final String WHERE = "WHERE";
|
||||
public static final String EXISTS = "EXISTS";
|
||||
public static final String NOT = "NOT";
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.AND;
|
||||
import static com.todoroo.andlib.data.sql.Constants.EXISTS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.LEFT_PARENTHESIS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.NOT;
|
||||
import static com.todoroo.andlib.data.sql.Constants.OR;
|
||||
import static com.todoroo.andlib.data.sql.Constants.RIGHT_PARENTHESIS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
|
||||
public abstract class Criterion {
|
||||
protected final Operator operator;
|
||||
|
||||
Criterion(Operator operator) {
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
public static Criterion and(final Criterion criterion, final Criterion... criterions) {
|
||||
return new Criterion(Operator.and) {
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
sb.append(criterion);
|
||||
for (Criterion c : criterions) {
|
||||
sb.append(SPACE).append(AND).append(SPACE).append(c);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Criterion or(final Criterion criterion, final Criterion... criterions) {
|
||||
return new Criterion(Operator.or) {
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
sb.append(criterion);
|
||||
for (Criterion c : criterions) {
|
||||
sb.append(SPACE).append(OR).append(SPACE).append(c.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Criterion exists(final Query query) {
|
||||
return new Criterion(Operator.exists) {
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
sb.append(EXISTS).append(SPACE).append(LEFT_PARENTHESIS).append(query).append(RIGHT_PARENTHESIS);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Criterion not(final Criterion criterion) {
|
||||
return new Criterion(Operator.not) {
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
sb.append(NOT).append(SPACE);
|
||||
criterion.populate(sb);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract void populate(StringBuilder sb);
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder(LEFT_PARENTHESIS);
|
||||
populate(builder);
|
||||
builder.append(RIGHT_PARENTHESIS);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.AS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
|
||||
public abstract class DBObject<T extends DBObject<?>> {
|
||||
protected String alias;
|
||||
protected final String expression;
|
||||
|
||||
protected DBObject(String expression){
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
public T as(String newAlias) {
|
||||
this.alias = newAlias;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public boolean hasAlias() {
|
||||
return alias != null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
DBObject dbObject = (DBObject) o;
|
||||
|
||||
if (alias != null ? !alias.equals(dbObject.alias) : dbObject.alias != null) return false;
|
||||
if (expression != null ? !expression.equals(dbObject.expression) : dbObject.expression != null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = alias != null ? alias.hashCode() : 0;
|
||||
result = 31 * result + (expression != null ? expression.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String toString() {
|
||||
StringBuilder sb = new StringBuilder(expression);
|
||||
if (hasAlias()) {
|
||||
sb.append(SPACE).append(AS).append(SPACE).append(alias);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
public class EqCriterion extends UnaryCriterion {
|
||||
EqCriterion(Field field, Object value) {
|
||||
super(field, Operator.eq, value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.AND;
|
||||
import static com.todoroo.andlib.data.sql.Constants.BETWEEN;
|
||||
import static com.todoroo.andlib.data.sql.Constants.COMMA;
|
||||
import static com.todoroo.andlib.data.sql.Constants.LEFT_PARENTHESIS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.RIGHT_PARENTHESIS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
|
||||
public class Field extends DBObject<Field> {
|
||||
|
||||
protected Field(String expression) {
|
||||
super(expression);
|
||||
}
|
||||
|
||||
public static Field field(String expression) {
|
||||
return new Field(expression);
|
||||
}
|
||||
|
||||
public Criterion eq(Object value) {
|
||||
return UnaryCriterion.eq(this, value);
|
||||
}
|
||||
|
||||
public Criterion neq(Object value) {
|
||||
return UnaryCriterion.neq(this, value);
|
||||
}
|
||||
|
||||
public Criterion gt(Object value) {
|
||||
return UnaryCriterion.gt(this, value);
|
||||
}
|
||||
|
||||
public Criterion lt(final Object value) {
|
||||
return UnaryCriterion.lt(this, value);
|
||||
}
|
||||
|
||||
public Criterion isNull() {
|
||||
return UnaryCriterion.isNull(this);
|
||||
}
|
||||
|
||||
public Criterion isNotNull() {
|
||||
return UnaryCriterion.isNotNull(this);
|
||||
}
|
||||
|
||||
public Criterion between(final Object lower, final Object upper) {
|
||||
final Field field = this;
|
||||
return new Criterion(null) {
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
sb.append(field).append(SPACE).append(BETWEEN).append(SPACE).append(lower).append(SPACE).append(AND)
|
||||
.append(SPACE).append(upper);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Criterion like(final String value) {
|
||||
return UnaryCriterion.like(this, value);
|
||||
}
|
||||
|
||||
public <T> Criterion in(final T... value) {
|
||||
final Field field = this;
|
||||
return new Criterion(Operator.in) {
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
sb.append(field).append(SPACE).append(Operator.in).append(SPACE).append(LEFT_PARENTHESIS);
|
||||
for (T t : value) {
|
||||
sb.append(t.toString()).append(COMMA);
|
||||
}
|
||||
sb.deleteCharAt(sb.length() - 1).append(RIGHT_PARENTHESIS);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Criterion in(final Query query) {
|
||||
final Field field = this;
|
||||
return new Criterion(Operator.in) {
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
sb.append(field).append(SPACE).append(Operator.in).append(SPACE).append(LEFT_PARENTHESIS).append(query)
|
||||
.append(RIGHT_PARENTHESIS);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GroupBy {
|
||||
private List<Field> fields = new ArrayList<Field>();
|
||||
|
||||
public static GroupBy groupBy(Field field) {
|
||||
GroupBy groupBy = new GroupBy();
|
||||
groupBy.fields.add(field);
|
||||
return groupBy;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.JOIN;
|
||||
import static com.todoroo.andlib.data.sql.Constants.ON;
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
|
||||
public class Join {
|
||||
private final Table joinTable;
|
||||
private final JoinType joinType;
|
||||
private final Criterion[] criterions;
|
||||
|
||||
private Join(Table table, JoinType joinType, Criterion... criterions) {
|
||||
joinTable = table;
|
||||
this.joinType = joinType;
|
||||
this.criterions = criterions;
|
||||
}
|
||||
|
||||
public static Join inner(Table expression, Criterion... criterions) {
|
||||
return new Join(expression, JoinType.INNER, criterions);
|
||||
}
|
||||
|
||||
public static Join left(Table table, Criterion... criterions) {
|
||||
return new Join(table, JoinType.LEFT, criterions);
|
||||
}
|
||||
|
||||
public static Join right(Table table, Criterion... criterions) {
|
||||
return new Join(table, JoinType.RIGHT, criterions);
|
||||
}
|
||||
|
||||
public static Join out(Table table, Criterion... criterions) {
|
||||
return new Join(table, JoinType.OUT, criterions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(joinType).append(SPACE).append(JOIN).append(SPACE).append(joinTable).append(SPACE).append(ON);
|
||||
for (Criterion criterion : criterions) {
|
||||
sb.append(SPACE).append(criterion);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
public enum JoinType {
|
||||
INNER, LEFT, RIGHT, OUT
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
public final class Operator {
|
||||
|
||||
private final String operator;
|
||||
public static final Operator eq = new Operator("=");
|
||||
public static final Operator neq = new Operator("<>");
|
||||
public static final Operator isNull = new Operator("IS NULL");
|
||||
public static final Operator isNotNull = new Operator("IS NOT NULL");
|
||||
public static final Operator gt = new Operator(">");
|
||||
public static final Operator lt = new Operator("<");
|
||||
public static final Operator gte = new Operator(">=");
|
||||
public static final Operator lte = new Operator("<=");
|
||||
public static final Operator and = new Operator("AND");
|
||||
public static final Operator or = new Operator("OR");
|
||||
public static final Operator not = new Operator("NOT");
|
||||
public static final Operator exists = new Operator("EXISTS");
|
||||
public static final Operator like = new Operator("LIKE");
|
||||
public static final Operator in = new Operator("IN");
|
||||
|
||||
private static final Map<Operator, Operator> contraryRegistry = new HashMap<Operator, Operator>();
|
||||
|
||||
static {
|
||||
contraryRegistry.put(eq, neq);
|
||||
contraryRegistry.put(neq, eq);
|
||||
contraryRegistry.put(isNull, isNotNull);
|
||||
contraryRegistry.put(isNotNull, isNull);
|
||||
contraryRegistry.put(gt, lte);
|
||||
contraryRegistry.put(lte, gt);
|
||||
contraryRegistry.put(lt, gte);
|
||||
contraryRegistry.put(gte, lt);
|
||||
}
|
||||
|
||||
private Operator(String operator) {
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
public Operator getContrary() {
|
||||
if(!contraryRegistry.containsKey(this)){
|
||||
Operator opposite = new Operator(not.toString() + SPACE + this.toString());
|
||||
contraryRegistry.put(this, opposite);
|
||||
contraryRegistry.put(opposite, this);
|
||||
}
|
||||
return contraryRegistry.get(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.operator.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
|
||||
public class Order {
|
||||
private final Field expression;
|
||||
private final OrderType orderType;
|
||||
|
||||
private Order(Field expression) {
|
||||
this(expression, OrderType.ASC);
|
||||
}
|
||||
|
||||
private Order(Field expression, OrderType orderType) {
|
||||
this.expression = expression;
|
||||
this.orderType = orderType;
|
||||
}
|
||||
|
||||
public static Order asc(Field expression) {
|
||||
return new Order(expression);
|
||||
}
|
||||
|
||||
public static Order desc(Field expression) {
|
||||
return new Order(expression, OrderType.DESC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return expression + SPACE + orderType;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
public enum OrderType {
|
||||
DESC, ASC
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.ALL;
|
||||
import static com.todoroo.andlib.data.sql.Constants.COMMA;
|
||||
import static com.todoroo.andlib.data.sql.Constants.FROM;
|
||||
import static com.todoroo.andlib.data.sql.Constants.GROUP_BY;
|
||||
import static com.todoroo.andlib.data.sql.Constants.LEFT_PARENTHESIS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.ORDER_BY;
|
||||
import static com.todoroo.andlib.data.sql.Constants.RIGHT_PARENTHESIS;
|
||||
import static com.todoroo.andlib.data.sql.Constants.SELECT;
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
import static com.todoroo.andlib.data.sql.Constants.WHERE;
|
||||
import static com.todoroo.andlib.data.sql.Table.table;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.todoroo.andlib.data.Property;
|
||||
|
||||
public class Query {
|
||||
|
||||
private Table table;
|
||||
private List<Criterion> criterions = new ArrayList<Criterion>();
|
||||
private List<Property<?>> fields = new ArrayList<Property<?>>();
|
||||
private List<Join> joins = new ArrayList<Join>();
|
||||
private List<Field> groupBies = new ArrayList<Field>();
|
||||
private List<Order> orders = new ArrayList<Order>();
|
||||
private List<Criterion> havings = new ArrayList<Criterion>();
|
||||
|
||||
private Query(Property<?>... fields) {
|
||||
this.fields.addAll(asList(fields));
|
||||
}
|
||||
|
||||
public static Query select(Property<?>... fields) {
|
||||
return new Query(fields);
|
||||
}
|
||||
|
||||
public Query from(Table fromTable) {
|
||||
this.table = fromTable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Query join(Join... join) {
|
||||
joins.addAll(asList(join));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Query where(Criterion criterion) {
|
||||
criterions.add(criterion);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Query groupBy(Field... groupBy) {
|
||||
groupBies.addAll(asList(groupBy));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Query orderBy(Order... order) {
|
||||
orders.addAll(asList(order));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Query appendSelectFields(Property<?>... selectFields) {
|
||||
this.fields.addAll(asList(selectFields));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return this == o || !(o == null || getClass() != o.getClass()) && this.toString().equals(o.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toString().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
visitSelectClause(sql);
|
||||
visitFromClause(sql);
|
||||
visitJoinClause(sql);
|
||||
visitWhereClause(sql);
|
||||
visitGroupByClause(sql);
|
||||
visitOrderByClause(sql);
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private void visitOrderByClause(StringBuilder sql) {
|
||||
if (orders.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sql.append(ORDER_BY);
|
||||
for (Order order : orders) {
|
||||
sql.append(SPACE).append(order).append(COMMA);
|
||||
}
|
||||
sql.deleteCharAt(sql.length() - 1).append(SPACE);
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
private void visitGroupByClause(StringBuilder sql) {
|
||||
if (groupBies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sql.append(GROUP_BY);
|
||||
for (Field groupBy : groupBies) {
|
||||
sql.append(SPACE).append(groupBy).append(COMMA);
|
||||
}
|
||||
sql.deleteCharAt(sql.length() - 1).append(SPACE);
|
||||
if (havings.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sql.append("HAVING");
|
||||
for (Criterion havingCriterion : havings) {
|
||||
sql.append(SPACE).append(havingCriterion).append(COMMA);
|
||||
}
|
||||
sql.deleteCharAt(sql.length() - 1).append(SPACE);
|
||||
}
|
||||
|
||||
private void visitWhereClause(StringBuilder sql) {
|
||||
if (criterions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sql.append(WHERE);
|
||||
for (Criterion criterion : criterions) {
|
||||
sql.append(SPACE).append(criterion).append(SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
private void visitJoinClause(StringBuilder sql) {
|
||||
for (Join join : joins) {
|
||||
sql.append(join).append(SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
private void visitFromClause(StringBuilder sql) {
|
||||
if (table == null) {
|
||||
return;
|
||||
}
|
||||
sql.append(FROM).append(SPACE).append(table).append(SPACE);
|
||||
}
|
||||
|
||||
private void visitSelectClause(StringBuilder sql) {
|
||||
sql.append(SELECT).append(SPACE);
|
||||
if (fields.isEmpty()) {
|
||||
sql.append(ALL).append(SPACE);
|
||||
return;
|
||||
}
|
||||
for (Field field : fields) {
|
||||
sql.append(field).append(COMMA);
|
||||
}
|
||||
sql.deleteCharAt(sql.length() - 1).append(SPACE);
|
||||
}
|
||||
|
||||
public Table as(String alias) {
|
||||
return table(LEFT_PARENTHESIS + this.toString() + RIGHT_PARENTHESIS).as(alias);
|
||||
}
|
||||
|
||||
public Query having(Criterion criterion) {
|
||||
this.havings.add(criterion);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of fields returned by this query
|
||||
* @return
|
||||
*/
|
||||
public Property<?>[] getFields() {
|
||||
return fields.toArray(new Property<?>[fields.size()]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
public class Table extends DBObject<Table> {
|
||||
|
||||
protected Table(String expression) {
|
||||
super(expression);
|
||||
}
|
||||
|
||||
public static Table table(String table) {
|
||||
return new Table(table);
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
protected String fieldExpression(String fieldName) {
|
||||
if (hasAlias()) {
|
||||
return alias + "." + fieldName;
|
||||
}
|
||||
return expression+"."+fieldName;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package com.todoroo.andlib.data.sql;
|
||||
|
||||
import static com.todoroo.andlib.data.sql.Constants.SPACE;
|
||||
|
||||
public class UnaryCriterion extends Criterion {
|
||||
protected final Field expression;
|
||||
protected final Object value;
|
||||
|
||||
UnaryCriterion(Field expression, Operator operator, Object value) {
|
||||
super(operator);
|
||||
this.expression = expression;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void populate(StringBuilder sb) {
|
||||
beforePopulateOperator(sb);
|
||||
populateOperator(sb);
|
||||
afterPopulateOperator(sb);
|
||||
}
|
||||
|
||||
public static Criterion eq(Field expression, Object value) {
|
||||
return new UnaryCriterion(expression, Operator.eq, value);
|
||||
}
|
||||
|
||||
protected void beforePopulateOperator(StringBuilder sb) {
|
||||
sb.append(expression);
|
||||
}
|
||||
|
||||
protected void populateOperator(StringBuilder sb) {
|
||||
sb.append(operator);
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
protected void afterPopulateOperator(StringBuilder sb) {
|
||||
if(value == null)
|
||||
return;
|
||||
else if(value instanceof String)
|
||||
sb.append("'").append(sanitize((String) value)).append("'");
|
||||
else
|
||||
sb.append(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the given input for SQL
|
||||
* @param input
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public static String sanitize(String input) {
|
||||
return input.replace("\\", "\\\\").replace("'", "\\'");
|
||||
}
|
||||
|
||||
public static Criterion neq(Field field, Object value) {
|
||||
return new UnaryCriterion(field, Operator.neq, value);
|
||||
}
|
||||
|
||||
public static Criterion gt(Field field, Object value) {
|
||||
return new UnaryCriterion(field, Operator.gt, value);
|
||||
}
|
||||
|
||||
public static Criterion lt(Field field, Object value) {
|
||||
return new UnaryCriterion(field, Operator.lt, value);
|
||||
}
|
||||
|
||||
public static Criterion isNull(Field field) {
|
||||
return new UnaryCriterion(field, Operator.isNull, null) {
|
||||
@Override
|
||||
protected void populateOperator(StringBuilder sb) {
|
||||
sb.append(SPACE).append(operator);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Criterion isNotNull(Field field) {
|
||||
return new UnaryCriterion(field, Operator.isNotNull, null) {
|
||||
@Override
|
||||
protected void populateOperator(StringBuilder sb) {
|
||||
sb.append(SPACE).append(operator);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Criterion like(Field field, String value) {
|
||||
return new UnaryCriterion(field, Operator.like, value) {
|
||||
@Override
|
||||
protected void populateOperator(StringBuilder sb) {
|
||||
sb.append(SPACE).append(operator).append(SPACE);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* A Dependency Injector knows how to inject certain dependencies based
|
||||
* on the field that is passed in.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public interface AbstractDependencyInjector {
|
||||
|
||||
/**
|
||||
* Gets the injected object for this field. If implementing class does not
|
||||
* know how to handle this dependency, it should return null
|
||||
*
|
||||
* @param object
|
||||
* object to perform dependency injection on
|
||||
* @param field
|
||||
* field tagged with {link Autowired} annotation
|
||||
* @return object to assign to this field, or null
|
||||
*/
|
||||
abstract Object getInjection(Object object, Field field);
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Autowired is an annotation that tells the dependency injector to
|
||||
* set up the class as appropriate.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Autowired {
|
||||
//
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Singleton class to manage current application context
|
||||
* b
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public final class ContextManager {
|
||||
|
||||
/**
|
||||
* Global application context
|
||||
*/
|
||||
private static Context context = null;
|
||||
|
||||
/**
|
||||
* Sets the global context
|
||||
* @param context
|
||||
*/
|
||||
public static void setContext(Context context) {
|
||||
ContextManager.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global context
|
||||
*/
|
||||
public static Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to read a string from the resources
|
||||
*
|
||||
* @param resid
|
||||
* @param parameters
|
||||
* @return
|
||||
*/
|
||||
public static String getString(int resId, Object... formatArgs) {
|
||||
return context.getString(resId, formatArgs);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Simple Dependency Injection Service for Android.
|
||||
* <p>
|
||||
* Add dependency injectors to the injector chain, then invoke this method
|
||||
* against classes you wish to perform dependency injection for.
|
||||
* <p>
|
||||
* All errors encountered are handled as warnings, so if dependency injection
|
||||
* seems to be failing, check the logs for more information.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class DependencyInjectionService {
|
||||
|
||||
private static final String QUALIFIED_PACKAGE = "com.t"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* Dependency injectors. Use getters and setters to modify this list
|
||||
*/
|
||||
private AbstractDependencyInjector[] injectors = {};
|
||||
|
||||
/**
|
||||
* Perform dependency injection in the caller object
|
||||
*
|
||||
* @param caller
|
||||
* object to perform DI on
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public void inject(Object caller) {
|
||||
|
||||
// Traverse through class and all parent classes, looking for
|
||||
// fields declared with the @Autowired annotation and using
|
||||
// dependency injection to set them as appropriate
|
||||
|
||||
Class<?> cls = caller.getClass();
|
||||
while(cls != null) {
|
||||
String packageName = cls.getPackage().getName();
|
||||
if(!packageName.startsWith(QUALIFIED_PACKAGE))
|
||||
break;
|
||||
|
||||
for(Field field : cls.getDeclaredFields()) {
|
||||
if(field.getAnnotation(Autowired.class) != null) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
handleField(caller, field);
|
||||
} catch (IllegalStateException e) {
|
||||
throw new RuntimeException(String.format("Unable to set field '%s' of type '%s'",
|
||||
field.getName(), field.getType()), e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException(String.format("Unable to set field '%s' of type '%s'",
|
||||
field.getName(), field.getType()), e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(String.format("Unable to set field '%s' of type '%s'",
|
||||
field.getName(), field.getType()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cls = cls.getSuperclass();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the appropriate dependency object based on the type
|
||||
* that this autowired field accepts
|
||||
*
|
||||
* @param caller
|
||||
* calling object
|
||||
* @param field
|
||||
* field to inject
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
private void handleField(Object caller, Field field)
|
||||
throws IllegalStateException, IllegalArgumentException,
|
||||
IllegalAccessException {
|
||||
|
||||
if(field.getType().isPrimitive())
|
||||
throw new IllegalStateException(String.format(
|
||||
"Tried to dependency-inject primative field '%s' of type '%s'",
|
||||
field.getName(), field.getType()));
|
||||
|
||||
// field has already been processed, ignore
|
||||
if (field.get(caller) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (AbstractDependencyInjector injector : injectors) {
|
||||
Object injection = injector.getInjection(caller, field);
|
||||
if (injection != null) {
|
||||
field.set(caller, injection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
String.format("No dependency injector found for autowired field '%s' in class '%s'",
|
||||
field.getName(), caller.getClass().getName()));
|
||||
}
|
||||
|
||||
// --- static methods
|
||||
|
||||
private static DependencyInjectionService instance = null;
|
||||
|
||||
DependencyInjectionService() {
|
||||
// prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of the dependency injection service.
|
||||
* @return
|
||||
*/
|
||||
public synchronized static DependencyInjectionService getInstance() {
|
||||
if(instance == null)
|
||||
instance = new DependencyInjectionService();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the array of installed injectors
|
||||
* @return
|
||||
*/
|
||||
public synchronized AbstractDependencyInjector[] getInjectors() {
|
||||
return injectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the array of installed injectors
|
||||
* @param injectors
|
||||
*/
|
||||
public synchronized void setInjectors(AbstractDependencyInjector[] injectors) {
|
||||
this.injectors = injectors;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.net.SocketTimeoutException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Exception handling utility class - reports and logs errors
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class ExceptionService {
|
||||
|
||||
@Autowired
|
||||
public ErrorReporter[] errorReporters;
|
||||
|
||||
@Autowired
|
||||
public Integer errorDialogTitleResource;
|
||||
|
||||
@Autowired
|
||||
public Integer errorDialogBodyGeneric;
|
||||
|
||||
@Autowired
|
||||
public Integer errorDialogBodyNullError;
|
||||
|
||||
@Autowired
|
||||
public Integer errorDialogBodySocketTimeout;
|
||||
|
||||
public ExceptionService() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the error via registered error handlers
|
||||
*
|
||||
* @param name Internal error name. Not displayed to user
|
||||
* @param error Exception encountered. Message will be displayed to user
|
||||
*/
|
||||
public void reportError(String name, Throwable error) {
|
||||
if(errorReporters == null)
|
||||
return;
|
||||
|
||||
for(ErrorReporter reporter : errorReporters)
|
||||
reporter.handleError(name, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display error dialog if context is activity and report error
|
||||
*
|
||||
* @param context Application Context
|
||||
* @param name Internal error name. Not displayed to user
|
||||
* @param error Exception encountered. Message will be displayed to user
|
||||
*/
|
||||
public void displayAndReportError(final Context context, String name, Throwable error) {
|
||||
if(context instanceof Activity) {
|
||||
final String messageToDisplay;
|
||||
|
||||
// pretty up the message when displaying to user
|
||||
if(error == null)
|
||||
messageToDisplay = context.getString(errorDialogBodyNullError);
|
||||
else if(error instanceof SocketTimeoutException)
|
||||
messageToDisplay = context.getString(errorDialogBodySocketTimeout);
|
||||
else
|
||||
messageToDisplay = context.getString(errorDialogBodyGeneric, error.getMessage());
|
||||
|
||||
((Activity)context).runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(errorDialogTitleResource)
|
||||
.setMessage(messageToDisplay)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} catch (Exception e) {
|
||||
// suppress errors during dialog creation
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reportError(name, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error reporter interface
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public interface ErrorReporter {
|
||||
public void handleError(String name, Throwable error);
|
||||
}
|
||||
|
||||
/**
|
||||
* AndroidLogReporter reports errors to LogCat
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class AndroidLogReporter implements ErrorReporter {
|
||||
|
||||
/**
|
||||
* Report the error to the logs
|
||||
*
|
||||
* @param name
|
||||
* @param error
|
||||
*/
|
||||
public void handleError(String name, Throwable error) {
|
||||
String tag = null;
|
||||
if(ContextManager.getContext() != null) {
|
||||
PackageManager pm = ContextManager.getContext().getPackageManager();
|
||||
try {
|
||||
String appName = pm.getApplicationInfo(ContextManager.getContext().
|
||||
getPackageName(), 0).name;
|
||||
tag = appName + "-" + name; //$NON-NLS-1$
|
||||
} catch (NameNotFoundException e) {
|
||||
// give up
|
||||
}
|
||||
}
|
||||
|
||||
if(tag == null)
|
||||
tag = "unknown-" + name; //$NON-NLS-1$
|
||||
|
||||
if(error == null)
|
||||
Log.e(tag, "Exception: " + name); //$NON-NLS-1$
|
||||
else
|
||||
Log.e(tag, error.toString(), error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncaught exception handler uses the exception utilities class to
|
||||
* report errors
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public static class TodorooUncaughtExceptionHandler implements UncaughtExceptionHandler {
|
||||
private UncaughtExceptionHandler defaultUEH;
|
||||
|
||||
@Autowired
|
||||
protected ExceptionService exceptionService;
|
||||
|
||||
public TodorooUncaughtExceptionHandler() {
|
||||
defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
public void uncaughtException(Thread thread, Throwable ex) {
|
||||
if(exceptionService != null)
|
||||
exceptionService.reportError("uncaught", ex); //$NON-NLS-1$
|
||||
defaultUEH.uncaughtException(thread, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HttpErrorException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = 5373340422464657279L;
|
||||
|
||||
public HttpErrorException(int code, String message) {
|
||||
super(String.format("%d %s", code, message)); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* RestClient allows Android to consume web requests.
|
||||
* <p>
|
||||
* Portions by Praeda:
|
||||
* http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple
|
||||
* -restful-client-at-android/
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class HttpRestClient implements RestClient {
|
||||
|
||||
private static final int HTTP_UNAVAILABLE_END = 599;
|
||||
private static final int HTTP_UNAVAILABLE_START = 500;
|
||||
private static final int HTTP_OK = 200;
|
||||
|
||||
private static final int TIMEOUT_MILLIS = 30000;
|
||||
|
||||
private static WeakReference<HttpClient> httpClient = null;
|
||||
|
||||
@Autowired
|
||||
private Boolean debug;
|
||||
|
||||
public HttpRestClient() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
private static String convertStreamToString(InputStream is) {
|
||||
/*
|
||||
* To convert the InputStream to String we use the
|
||||
* BufferedReader.readLine() method. We iterate until the BufferedReader
|
||||
* return null which means there's no more data to read. Each line will
|
||||
* appended to a StringBuilder and returned as String.
|
||||
*/
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is), 16384);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
String line = null;
|
||||
try {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line + "\n"); //$NON-NLS-1$
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private synchronized static void initializeHttpClient() {
|
||||
if (httpClient == null || httpClient.get() == null) {
|
||||
HttpParams params = new BasicHttpParams();
|
||||
HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_MILLIS);
|
||||
HttpConnectionParams.setSoTimeout(params, TIMEOUT_MILLIS);
|
||||
httpClient = new WeakReference<HttpClient>(new DefaultHttpClient(params));
|
||||
}
|
||||
}
|
||||
|
||||
private String processHttpResponse(HttpResponse response) throws IOException {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if(statusCode >= HTTP_UNAVAILABLE_START && statusCode <= HTTP_UNAVAILABLE_END) {
|
||||
throw new HttpUnavailableException();
|
||||
} else if(statusCode != HTTP_OK) {
|
||||
throw new HttpErrorException(response.getStatusLine().getStatusCode(),
|
||||
response.getStatusLine().getReasonPhrase());
|
||||
}
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
if (entity != null) {
|
||||
InputStream contentStream = entity.getContent();
|
||||
try {
|
||||
return convertStreamToString(contentStream);
|
||||
} finally {
|
||||
contentStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue an HTTP GET for the given URL, return the response
|
||||
*
|
||||
* @param url url with url-encoded params
|
||||
* @return response, or null if there was no response
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized String get(String url) throws IOException {
|
||||
initializeHttpClient();
|
||||
|
||||
if(debug)
|
||||
Log.d("http-rest-client-get", url); //$NON-NLS-1$
|
||||
|
||||
try {
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
HttpResponse response = httpClient.get().execute(httpGet);
|
||||
|
||||
return processHttpResponse(response);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
IOException ioException = new IOException(e.getMessage());
|
||||
ioException.initCause(e);
|
||||
throw ioException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue an HTTP POST for the given URL, return the response
|
||||
*
|
||||
* @param url
|
||||
* @param data
|
||||
* url-encoded data
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized String post(String url, String data) throws IOException {
|
||||
initializeHttpClient();
|
||||
|
||||
if(debug)
|
||||
Log.d("http-rest-client-post", url + " | " + data); //$NON-NLS-1$ //$NON-NLS-2$
|
||||
|
||||
try {
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setEntity(new StringEntity(data));
|
||||
HttpResponse response = httpClient.get().execute(httpPost);
|
||||
|
||||
return processHttpResponse(response);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
IOException ioException = new IOException(e.getMessage());
|
||||
ioException.initCause(e);
|
||||
throw ioException;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Exception displayed when a 500 error is received on an HTTP invocation
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class HttpUnavailableException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = 5373340422464657279L;
|
||||
|
||||
public HttpUnavailableException() {
|
||||
super();
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Sorry, our servers are experiencing some issues. Please try again later!"; //$NON-NLS-1$ // FIXME
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Notification Manager stub
|
||||
*
|
||||
* @author timsu
|
||||
*
|
||||
*/
|
||||
public interface NotificationManager {
|
||||
|
||||
public void cancel(int id);
|
||||
|
||||
public void cancelAll();
|
||||
|
||||
public void notify(int id, Notification notification);
|
||||
|
||||
/**
|
||||
* Instantiation of notification manager that passes through to
|
||||
* Android's notification manager
|
||||
*
|
||||
* @author timsu
|
||||
*
|
||||
*/
|
||||
public static class AndroidNotificationManager implements NotificationManager {
|
||||
final android.app.NotificationManager nm;
|
||||
public AndroidNotificationManager(Context context) {
|
||||
nm = (android.app.NotificationManager)
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
public void cancel(int id) {
|
||||
nm.cancel(id);
|
||||
}
|
||||
|
||||
public void cancelAll() {
|
||||
nm.cancelAll();
|
||||
}
|
||||
|
||||
public void notify(int id, Notification notification) {
|
||||
nm.notify(id, notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.todoroo.andlib.service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* RestClient stub invokes the HTML requests as desired
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public interface RestClient {
|
||||
public String get(String url) throws IOException;
|
||||
public String post(String url, String data) throws IOException;
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
package com.todoroo.andlib.utility;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.NetworkInfo.State;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
import com.todoroo.andlib.service.ExceptionService;
|
||||
|
||||
/**
|
||||
* Android Utility Classes
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class AndroidUtilities {
|
||||
|
||||
private static class ExceptionHelper {
|
||||
@Autowired
|
||||
public ExceptionService exceptionService;
|
||||
|
||||
public ExceptionHelper() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if we're connected to the internet
|
||||
*/
|
||||
public static boolean isConnected(Context context) {
|
||||
ConnectivityManager manager = (ConnectivityManager)
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo info = manager.getActiveNetworkInfo();
|
||||
if (info == null)
|
||||
return false;
|
||||
if (info.getState() != State.CONNECTED)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Fetch the image specified by the given url */
|
||||
public static Bitmap fetchImage(URL url) throws IOException {
|
||||
InputStream is = null;
|
||||
try {
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.connect();
|
||||
is = conn.getInputStream();
|
||||
BufferedInputStream bis = new BufferedInputStream(is, 16384);
|
||||
try {
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(bis);
|
||||
return bitmap;
|
||||
} finally {
|
||||
bis.close();
|
||||
}
|
||||
} finally {
|
||||
if(is != null)
|
||||
is.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the given intent, handling security exceptions if they arise
|
||||
*
|
||||
* @param context
|
||||
* @param intent
|
||||
*/
|
||||
public static void startExternalIntent(Context context, Intent intent) {
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (SecurityException e) {
|
||||
ExceptionHelper helper = new ExceptionHelper();
|
||||
helper.exceptionService.displayAndReportError(context,
|
||||
"start-external-intent-" + intent.toString(), //$NON-NLS-1$
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the given intent, handling security exceptions if they arise
|
||||
*
|
||||
* @param activity
|
||||
* @param intent
|
||||
* @param requestCode
|
||||
*/
|
||||
public static void startExternalIntentForResult(
|
||||
Activity activity, Intent intent, int requestCode) {
|
||||
try {
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (SecurityException e) {
|
||||
ExceptionHelper helper = new ExceptionHelper();
|
||||
helper.exceptionService.displayAndReportError(activity,
|
||||
"start-external-intent-" + intent.toString(), //$NON-NLS-1$
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (c) 2009, Todoroo Inc
|
||||
* All Rights Reserved
|
||||
* http://www.todoroo.com
|
||||
*/
|
||||
package com.todoroo.andlib.utility;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
|
||||
|
||||
public class DateUtilities {
|
||||
|
||||
@Autowired
|
||||
public Integer yearsResource;
|
||||
|
||||
@Autowired
|
||||
public Integer monthsResource;
|
||||
|
||||
@Autowired
|
||||
public Integer daysResource;
|
||||
|
||||
@Autowired
|
||||
public Integer hoursResource;
|
||||
|
||||
@Autowired
|
||||
public Integer minutesResource;
|
||||
|
||||
@Autowired
|
||||
public Integer secondsResource;
|
||||
|
||||
@Autowired
|
||||
public Integer daysAbbrevResource;
|
||||
|
||||
@Autowired
|
||||
public Integer hoursAbbrevResource;
|
||||
|
||||
@Autowired
|
||||
public Integer minutesAbbrevResource;
|
||||
|
||||
@Autowired
|
||||
public Integer secondsAbbrevResource;
|
||||
|
||||
public DateUtilities() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
/* ======================================================================
|
||||
* ============================================================ unix time
|
||||
* ====================================================================== */
|
||||
|
||||
/** Convert unixtime into date */
|
||||
public static final Date unixtimeToDate(int seconds) {
|
||||
if(seconds == 0)
|
||||
return null;
|
||||
return new Date(seconds * 1000L);
|
||||
}
|
||||
|
||||
/** Convert date into unixtime */
|
||||
public static final int dateToUnixtime(Date date) {
|
||||
if(date == null)
|
||||
return 0;
|
||||
return (int)(date.getTime() / 1000);
|
||||
}
|
||||
|
||||
/** Returns unixtime for current time */
|
||||
public static final int now() {
|
||||
return (int) (System.currentTimeMillis() / 1000L);
|
||||
}
|
||||
|
||||
/* ======================================================================
|
||||
* =========================================================== formatters
|
||||
* ====================================================================== */
|
||||
|
||||
/**
|
||||
* Convenience method for dropping the preposition argument.
|
||||
*/
|
||||
public String getDurationString(Resources r, int timeInSeconds,
|
||||
int unitsToShow) {
|
||||
return getDurationString(r, timeInSeconds, unitsToShow, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a time into the format: 5 days, 3 hours, 2 minutes
|
||||
*
|
||||
* @param r Resources to get strings from
|
||||
* @param timeInSeconds
|
||||
* @param unitsToShow number of units to show (i.e. if 2, then 5 hours
|
||||
* 3 minutes 2 seconds is truncated to 5 hours 3 minutes)
|
||||
* @param withPreposition whether there is a preceding preposition
|
||||
* @return
|
||||
*/
|
||||
public String getDurationString(Resources r, int timeInSeconds,
|
||||
int unitsToShow, boolean withPreposition) {
|
||||
int years, months, days, hours, minutes, seconds;
|
||||
short unitsDisplayed = 0;
|
||||
timeInSeconds = Math.abs(timeInSeconds);
|
||||
|
||||
if(timeInSeconds == 0)
|
||||
return r.getQuantityString(secondsResource, 0, 0);
|
||||
|
||||
Date now = new Date(80, 0, 1);
|
||||
Date then = unixtimeToDate((int)(now.getTime() / 1000L) + timeInSeconds);
|
||||
|
||||
years = then.getYear() - now.getYear();
|
||||
months = then.getMonth() - now.getMonth();
|
||||
days = then.getDate() - now.getDate();
|
||||
hours = then.getHours() - now.getHours();
|
||||
minutes = then.getMinutes() - now.getMinutes();
|
||||
seconds = then.getSeconds() - now.getSeconds();
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
unitsDisplayed = displayUnits(r, yearsResource, unitsToShow, years, months >= 6,
|
||||
unitsDisplayed, result);
|
||||
unitsDisplayed = displayUnits(r, monthsResource, unitsToShow, months, days >= 15,
|
||||
unitsDisplayed, result);
|
||||
unitsDisplayed = displayUnits(r, daysResource, unitsToShow, days, hours >= 12,
|
||||
unitsDisplayed, result);
|
||||
unitsDisplayed = displayUnits(r, hoursResource, unitsToShow, hours, minutes >= 30,
|
||||
unitsDisplayed, result);
|
||||
unitsDisplayed = displayUnits(r, minutesResource, unitsToShow, minutes, seconds >= 30,
|
||||
unitsDisplayed, result);
|
||||
unitsDisplayed = displayUnits(r, secondsResource, unitsToShow, seconds, false,
|
||||
unitsDisplayed, result);
|
||||
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
/** Display units, rounding up if necessary. Returns units to show */
|
||||
private short displayUnits(Resources r, int resource, int unitsToShow, int value,
|
||||
boolean shouldRound, short unitsDisplayed, StringBuilder result) {
|
||||
if(unitsDisplayed < unitsToShow && value > 0) {
|
||||
// round up if needed
|
||||
if(unitsDisplayed + 1 == unitsToShow && shouldRound)
|
||||
value++;
|
||||
result.append(r.getQuantityString(resource, value, value)).
|
||||
append(' ');
|
||||
unitsDisplayed++;
|
||||
}
|
||||
return unitsDisplayed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a time into the format: 5 days, 3 hrs, 2 min
|
||||
*
|
||||
* @param r Resources to get strings from
|
||||
* @param timeInSeconds
|
||||
* @param unitsToShow number of units to show (i.e. if 2, then 5 hours
|
||||
* 3 minutes 2 seconds is truncated to 5 hours 3 minutes)
|
||||
* @return
|
||||
*/
|
||||
public String getAbbreviatedDurationString(Resources r, int timeInSeconds,
|
||||
int unitsToShow) {
|
||||
short days, hours, minutes, seconds;
|
||||
short unitsDisplayed = 0;
|
||||
timeInSeconds = Math.abs(timeInSeconds);
|
||||
|
||||
if(timeInSeconds == 0)
|
||||
return r.getQuantityString(secondsAbbrevResource, 0, 0);
|
||||
|
||||
days = (short)(timeInSeconds / 24 / 3600);
|
||||
timeInSeconds -= days*24*3600;
|
||||
hours = (short)(timeInSeconds / 3600);
|
||||
timeInSeconds -= hours * 3600;
|
||||
minutes = (short)(timeInSeconds / 60);
|
||||
timeInSeconds -= minutes * 60;
|
||||
seconds = (short)timeInSeconds;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
if(days > 0) {
|
||||
// round up if needed
|
||||
if(unitsDisplayed == unitsToShow && hours >= 12)
|
||||
days++;
|
||||
result.append(r.getQuantityString(daysAbbrevResource, days, days)).
|
||||
append(' ');
|
||||
unitsDisplayed++;
|
||||
}
|
||||
if(unitsDisplayed < unitsToShow && hours > 0) {
|
||||
// round up if needed
|
||||
if(unitsDisplayed == unitsToShow && minutes >= 30)
|
||||
days++;
|
||||
result.append(r.getQuantityString(hoursAbbrevResource, hours,
|
||||
hours)).
|
||||
append(' ');
|
||||
unitsDisplayed++;
|
||||
}
|
||||
if(unitsDisplayed < unitsToShow && minutes > 0) {
|
||||
// round up if needed
|
||||
if(unitsDisplayed == unitsToShow && seconds >= 30)
|
||||
days++;
|
||||
result.append(r.getQuantityString(minutesAbbrevResource, minutes,
|
||||
minutes)).append(' ');
|
||||
unitsDisplayed++;
|
||||
}
|
||||
if(unitsDisplayed < unitsToShow && seconds > 0) {
|
||||
result.append(r.getQuantityString(secondsAbbrevResource, seconds,
|
||||
seconds)).append(' ');
|
||||
}
|
||||
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
package com.todoroo.andlib.utility;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.View;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
|
||||
public class DialogUtilities {
|
||||
|
||||
@Autowired
|
||||
public Integer informationDialogTitleResource;
|
||||
|
||||
public DialogUtilities() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog box with a EditText and an ok / cancel
|
||||
*
|
||||
* @param activity
|
||||
* @param text
|
||||
* @param okListener
|
||||
*/
|
||||
public void viewDialog(final Activity activity, final String text,
|
||||
final View view, final DialogInterface.OnClickListener okListener,
|
||||
final DialogInterface.OnClickListener cancelListener) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(informationDialogTitleResource)
|
||||
.setMessage(text)
|
||||
.setView(view)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(android.R.string.ok, okListener)
|
||||
.setNegativeButton(android.R.string.cancel, cancelListener)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog box with an OK button
|
||||
*
|
||||
* @param activity
|
||||
* @param text
|
||||
* @param okListener
|
||||
*/
|
||||
public void okDialog(final Activity activity, final String text,
|
||||
final DialogInterface.OnClickListener okListener) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(informationDialogTitleResource)
|
||||
.setMessage(text)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(android.R.string.ok, okListener)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog box with an OK button
|
||||
*
|
||||
* @param activity
|
||||
* @param text
|
||||
* @param okListener
|
||||
*/
|
||||
public void okDialog(final Activity activity, final int icon, final String text,
|
||||
final DialogInterface.OnClickListener okListener) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(informationDialogTitleResource)
|
||||
.setMessage(text)
|
||||
.setIcon(icon)
|
||||
.setPositiveButton(android.R.string.ok, okListener)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog box with OK and Cancel buttons and custom title
|
||||
*
|
||||
* @param activity
|
||||
* @param title
|
||||
* @param text
|
||||
* @param okListener
|
||||
* @param cancelListener
|
||||
*/
|
||||
public void okCancelDialog(final Activity activity, final String title,
|
||||
final String text, final DialogInterface.OnClickListener okListener,
|
||||
final DialogInterface.OnClickListener cancelListener) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(title)
|
||||
.setMessage(text)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(android.R.string.ok, okListener)
|
||||
.setNegativeButton(android.R.string.cancel, cancelListener)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog box with OK and Cancel buttons
|
||||
*
|
||||
* @param activity
|
||||
* @param text
|
||||
* @param okListener
|
||||
* @param cancelListener
|
||||
*/
|
||||
public void okCancelDialog(final Activity activity, final String text,
|
||||
final DialogInterface.OnClickListener okListener,
|
||||
final DialogInterface.OnClickListener cancelListener) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(informationDialogTitleResource)
|
||||
.setMessage(text)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(android.R.string.ok, okListener)
|
||||
.setNegativeButton(android.R.string.cancel, cancelListener)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress dialog. Must be run on the UI thread
|
||||
* @param context
|
||||
* @param text
|
||||
* @return
|
||||
*/
|
||||
public ProgressDialog progressDialog(Context context, String text) {
|
||||
ProgressDialog dialog = new ProgressDialog(context);
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
dialog.setMessage(text);
|
||||
dialog.show();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a dialog off the UI thread
|
||||
*
|
||||
* @param activity
|
||||
* @param dialog
|
||||
*/
|
||||
public void dismissDialog(Activity activity, final ProgressDialog dialog) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
dialog.dismiss();
|
||||
} catch (Exception e) {
|
||||
// could have killed activity
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.todoroo.andlib.utility;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* E-mail Validator Copyright 2008 Les Hazlewood
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public final class EmailValidator {
|
||||
|
||||
// RFC 2822 2.2.2 Structured Header Field Bodies
|
||||
private static final String wsp = "[ \\t]"; // space or tab
|
||||
private static final String fwsp = wsp + "*";
|
||||
|
||||
// RFC 2822 3.2.1 Primitive tokens
|
||||
private static final String dquote = "\\\"";
|
||||
// ASCII Control characters excluding white space:
|
||||
private static final String noWsCtl = "\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F";
|
||||
// all ASCII characters except CR and LF:
|
||||
private static final String asciiText = "[\\x01-\\x09\\x0B\\x0C\\x0E-\\x7F]";
|
||||
|
||||
// RFC 2822 3.2.2 Quoted characters:
|
||||
// single backslash followed by a text char
|
||||
private static final String quotedPair = "(\\\\" + asciiText + ")";
|
||||
|
||||
// RFC 2822 3.2.4 Atom:
|
||||
private static final String atext = "[a-zA-Z0-9\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\_\\`\\{\\|\\}\\~]";
|
||||
private static final String dotAtomText = atext + "+" + "(" + "\\." + atext
|
||||
+ "+)*";
|
||||
private static final String dotAtom = fwsp + "(" + dotAtomText + ")" + fwsp;
|
||||
|
||||
// RFC 2822 3.2.5 Quoted strings:
|
||||
// noWsCtl and the rest of ASCII except the doublequote and backslash
|
||||
// characters:
|
||||
private static final String qtext = "[" + noWsCtl
|
||||
+ "\\x21\\x23-\\x5B\\x5D-\\x7E]";
|
||||
private static final String qcontent = "(" + qtext + "|" + quotedPair + ")";
|
||||
private static final String quotedString = dquote + "(" + fwsp + qcontent
|
||||
+ ")*" + fwsp + dquote;
|
||||
|
||||
// RFC 1035 tokens for domain names:
|
||||
private static final String letter = "[a-zA-Z]";
|
||||
private static final String letDig = "[a-zA-Z0-9]";
|
||||
private static final String letDigHyp = "[a-zA-Z0-9-]";
|
||||
private static final String rfcLabel = letDig + "(" + letDigHyp + "{0,61}"
|
||||
+ letDig + ")?";
|
||||
private static final String rfc1035DomainName = rfcLabel + "(\\."
|
||||
+ rfcLabel + ")*\\." + letter + "{2,6}";
|
||||
|
||||
private static final String domain = rfc1035DomainName;
|
||||
|
||||
private static final String localPart = "((" + dotAtom + ")|("
|
||||
+ quotedString + "))";
|
||||
private static final String addrSpec = localPart + "@" + domain;
|
||||
|
||||
// now compile a pattern for efficient re-use:
|
||||
// if we're allowing quoted identifiers or not:
|
||||
private static final String patternString = addrSpec;
|
||||
public static final Pattern VALID_PATTERN = Pattern.compile(patternString);
|
||||
|
||||
|
||||
public static boolean validateEmail(String value) {
|
||||
return VALID_PATTERN.matcher(value).matches();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.todoroo.andlib.utility;
|
||||
|
||||
/**
|
||||
* Pair utility class
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
* @param <L>
|
||||
* @param <R>
|
||||
*/
|
||||
public class Pair<L, R> {
|
||||
|
||||
private final L left;
|
||||
private final R right;
|
||||
|
||||
public R getRight() {
|
||||
return right;
|
||||
}
|
||||
|
||||
public L getLeft() {
|
||||
return left;
|
||||
}
|
||||
|
||||
public Pair(final L left, final R right) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
public static <A, B> Pair<A, B> create(A left, B right) {
|
||||
return new Pair<A, B>(left, right);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if (!(o instanceof Pair<?, ?>))
|
||||
return false;
|
||||
|
||||
final Pair<?, ?> other = (Pair<?, ?>) o;
|
||||
return equal(getLeft(), other.getLeft()) && equal(getRight(), other.getRight());
|
||||
}
|
||||
|
||||
public static final boolean equal(Object o1, Object o2) {
|
||||
if (o1 == null) {
|
||||
return o2 == null;
|
||||
}
|
||||
return o1.equals(o2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hLeft = getLeft() == null ? 0 : getLeft().hashCode();
|
||||
int hRight = getRight() == null ? 0 : getRight().hashCode();
|
||||
|
||||
return hLeft + (57 * hRight);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
package com.todoroo.andlib.utility;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* SoftHashMap from javaspecialists.eu Issue 98
|
||||
* <http://www.javaspecialists.eu/archive/Issue098.html>
|
||||
*
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
*/
|
||||
public class SoftHashMap<K, V> extends AbstractMap<K, V> implements
|
||||
Serializable {
|
||||
private static final long serialVersionUID = -3796460667941300642L;
|
||||
|
||||
/** The internal HashMap that will hold the SoftReference. */
|
||||
private final Map<K, SoftReference<V>> hash = new HashMap<K, SoftReference<V>>();
|
||||
|
||||
private final Map<SoftReference<V>, K> reverseLookup = new HashMap<SoftReference<V>, K>();
|
||||
|
||||
/** Reference queue for cleared SoftReference objects. */
|
||||
protected final ReferenceQueue<V> queue = new ReferenceQueue<V>();
|
||||
|
||||
@Override
|
||||
public V get(Object key) {
|
||||
expungeStaleEntries();
|
||||
V result = null;
|
||||
// We get the SoftReference represented by that key
|
||||
SoftReference<V> soft_ref = hash.get(key);
|
||||
if (soft_ref != null) {
|
||||
// From the SoftReference we get the value, which can be
|
||||
// null if it has been garbage collected
|
||||
result = soft_ref.get();
|
||||
if (result == null) {
|
||||
// If the value has been garbage collected, remove the
|
||||
// entry from the HashMap.
|
||||
hash.remove(key);
|
||||
reverseLookup.remove(soft_ref);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void expungeStaleEntries() {
|
||||
Reference<? extends V> sv;
|
||||
while ((sv = queue.poll()) != null) {
|
||||
hash.remove(reverseLookup.remove(sv));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) {
|
||||
expungeStaleEntries();
|
||||
SoftReference<V> soft_ref = new SoftReference<V>(value, queue);
|
||||
reverseLookup.put(soft_ref, key);
|
||||
SoftReference<V> result = hash.put(key, soft_ref);
|
||||
if (result == null)
|
||||
return null;
|
||||
reverseLookup.remove(result);
|
||||
return result.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(Object key) {
|
||||
expungeStaleEntries();
|
||||
SoftReference<V> result = hash.remove(key);
|
||||
if (result == null)
|
||||
return null;
|
||||
return result.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
hash.clear();
|
||||
reverseLookup.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
expungeStaleEntries();
|
||||
return hash.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the key/values in the map at the point of calling.
|
||||
* However, setValue still sets the value in the actual SoftHashMap.
|
||||
*/
|
||||
@Override
|
||||
public Set<Entry<K, V>> entrySet() {
|
||||
expungeStaleEntries();
|
||||
Set<Entry<K, V>> result = new LinkedHashSet<Entry<K, V>>();
|
||||
for (final Entry<K, SoftReference<V>> entry : hash.entrySet()) {
|
||||
final V value = entry.getValue().get();
|
||||
if (value != null) {
|
||||
result.add(new Entry<K, V>() {
|
||||
public K getKey() {
|
||||
return entry.getKey();
|
||||
}
|
||||
|
||||
public V getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public V setValue(V v) {
|
||||
entry.setValue(new SoftReference<V>(v, queue));
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,447 @@
|
||||
package com.todoroo.andlib.utility;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
|
||||
/**
|
||||
* <p>UserTask enables proper and easy use of the UI thread. This class allows to
|
||||
* perform background operations and publish results on the UI thread without
|
||||
* having to manipulate threads and/or handlers.</p>
|
||||
*
|
||||
* <p>A user task is defined by a computation that runs on a background thread and
|
||||
* whose result is published on the UI thread. A user task is defined by 3 generic
|
||||
* types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>,
|
||||
* and 4 steps, called <code>begin</code>, <code>doInBackground</code>,
|
||||
* <code>processProgress<code> and <code>end</code>.</p>
|
||||
*
|
||||
* <h2>Usage</h2>
|
||||
* <p>UserTask must be subclassed to be used. The subclass will override at least
|
||||
* one method ({@link #doInBackground(Object[])}), and most often will override a
|
||||
* second one ({@link #end(Object)}.)</p>
|
||||
*
|
||||
* <p>Here is an example of subclassing:</p>
|
||||
* <pre>
|
||||
* private class DownloadFilesTask extends UserTask<URL, Integer, Long> {
|
||||
* public File doInBackground(URL... urls) {
|
||||
* int count = urls.length;
|
||||
* long totalSize = 0;
|
||||
* for (int i = 0; i < count; i++) {
|
||||
* totalSize += Downloader.downloadFile(urls[i]);
|
||||
* publishProgress((int) ((i / (float) count) * 100));
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* public void processProgress(Integer... progress) {
|
||||
* setProgressPercent(progress[0]);
|
||||
* }
|
||||
*
|
||||
* public void end(Long result) {
|
||||
* showDialog("Downloaded " + result + " bytes");
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Once created, a task is executed very simply:</p>
|
||||
* <pre>
|
||||
* new DownloadFilesTask().execute(new URL[] { ... });
|
||||
* </pre>
|
||||
*
|
||||
* <h2>User task's generic types</h2>
|
||||
* <p>The three types used by a user task are the following:</p>
|
||||
* <ol>
|
||||
* <li><code>Params</code>, the type of the parameters sent to the task upon
|
||||
* execution.</li>
|
||||
* <li><code>Progress</code>, the type of the progress units published during
|
||||
* the background computation.</li>
|
||||
* <li><code>Result</code>, the type of the result of the background
|
||||
* computation.</li>
|
||||
* </ol>
|
||||
* <p>Not all types are always used by a user task. To mark a type as unused,
|
||||
* simply use the type {@link Void}:</p>
|
||||
* <pre>
|
||||
* private class MyTask extends UserTask<Void, Void, Void) { ... }
|
||||
* </pre>
|
||||
*
|
||||
* <h2>The 4 steps</h2>
|
||||
* <p>When a user task is executed, the task goes through 4 steps:</p>
|
||||
* <ol>
|
||||
* <li>{@link #begin()}, invoked on the UI thread immediately after the task
|
||||
* is executed. This step is normally used to setup the task, for instance by
|
||||
* showing a progress bar in the user interface.</li>
|
||||
* <li>{@link #doInBackground(Object[])}, invoked on the background thread
|
||||
* immediately after {@link #begin()} finishes executing. This step is used
|
||||
* to perform background computation that can take a long time. The parameters
|
||||
* of the user task are passed to this step. The result of the computation must
|
||||
* be returned by this step and will be passed back to the last step. This step
|
||||
* can also use {@link #publishProgress(Object[])} to publish one or more units
|
||||
* of progress. These values are published on the UI thread, in the
|
||||
* {@link #processProgress(Object[])} step.</li>
|
||||
* <li>{@link #processProgress(Object[])}, invoked on the UI thread after a
|
||||
* call to {@link #publishProgress(Object[])}. The timing of the execution is
|
||||
* undefined. This method is used to display any form of progress in the user
|
||||
* interface while the background computation is still executing. For instance,
|
||||
* it can be used to animate a progress bar or show logs in a text field.</li>
|
||||
* <li>{@link #end(Object)}, invoked on the UI thread after the background
|
||||
* computation finishes. The result of the background computation is passed to
|
||||
* this step as a parameter.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Threading rules</h2>
|
||||
* <p>There are a few threading rules that must be followed for this class to
|
||||
* work properly:</p>
|
||||
* <ul>
|
||||
* <li>The task instance must be created on the UI thread.</li>
|
||||
* <li>{@link #execute(Object[])} must be invoked on the UI thread.</li>
|
||||
* <li>Do not call {@link #begin()}, {@link #end(Object)},
|
||||
* {@link #doInBackground(Object[])}, {@link #processProgress(Object[])}
|
||||
* manually.</li>
|
||||
* <li>The task can be executed only once (an exception will be thrown if
|
||||
* a second execution is attempted.)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
public abstract class UserTask<Params, Progress, Result> {
|
||||
private static final String LOG_TAG = "UserTask";
|
||||
|
||||
private static final int CORE_POOL_SIZE = 4;
|
||||
private static final int MAXIMUM_POOL_SIZE = 10;
|
||||
private static final int KEEP_ALIVE = 10;
|
||||
|
||||
private static final BlockingQueue<Runnable> sWorkQueue =
|
||||
new LinkedBlockingQueue<Runnable>(MAXIMUM_POOL_SIZE);
|
||||
|
||||
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
|
||||
private final AtomicInteger mCount = new AtomicInteger(1);
|
||||
|
||||
public Thread newThread(Runnable r) {
|
||||
final Thread thread = new Thread(r, "UserTask #" + mCount.getAndIncrement());
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
return thread;
|
||||
}
|
||||
};
|
||||
|
||||
private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
|
||||
MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
|
||||
|
||||
private static final int MESSAGE_POST_RESULT = 0x1;
|
||||
private static final int MESSAGE_POST_PROGRESS = 0x2;
|
||||
|
||||
protected static InternalHandler sHandler;
|
||||
|
||||
private final WorkerRunnable<Params, Result> mWorker;
|
||||
private final FutureTask<Result> mFuture;
|
||||
|
||||
private volatile Status mStatus = Status.PENDING;
|
||||
|
||||
/**
|
||||
* Indicates the current status of the task. Each status will be set only once
|
||||
* during the lifetime of a task.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* Indicates that the task has not been executed yet.
|
||||
*/
|
||||
PENDING,
|
||||
/**
|
||||
* Indicates that the task is running.
|
||||
*/
|
||||
RUNNING,
|
||||
/**
|
||||
* Indicates that {@link UserTask#end(Object)} has finished.
|
||||
*/
|
||||
FINISHED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user task. This constructor must be invoked on the UI thread.
|
||||
*/
|
||||
public UserTask() {
|
||||
if (sHandler == null) {
|
||||
sHandler = new InternalHandler();
|
||||
}
|
||||
|
||||
mWorker = new WorkerRunnable<Params, Result>() {
|
||||
public Result call() throws Exception {
|
||||
return doInBackground(mParams);
|
||||
}
|
||||
};
|
||||
|
||||
mFuture = new FutureTask<Result>(mWorker) {
|
||||
@Override
|
||||
protected void done() {
|
||||
Result result = null;
|
||||
try {
|
||||
result = get();
|
||||
} catch (InterruptedException e) {
|
||||
android.util.Log.w(LOG_TAG, e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException("An error occured while executing doInBackground()",
|
||||
e.getCause());
|
||||
} catch (CancellationException e) {
|
||||
return;
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException("An error occured while executing "
|
||||
+ "doInBackground()", t);
|
||||
}
|
||||
|
||||
final Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
|
||||
new UserTaskResult<Result>(UserTask.this, result));
|
||||
message.sendToTarget();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current status of this task.
|
||||
*
|
||||
* @return The current status.
|
||||
*/
|
||||
public final Status getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to perform a computation on a background thread. The
|
||||
* specified parameters are the parameters passed to {@link #execute(Object[])}
|
||||
* by the caller of this task.
|
||||
*
|
||||
* This method can call {@link #publishProgress(Object[])} to publish updates
|
||||
* on the UI thread.
|
||||
*
|
||||
* @params params The parameters of the task.
|
||||
*
|
||||
* @return A result, defined by the subclass of this task.
|
||||
*
|
||||
* @see #begin()
|
||||
* @see #end(Object)
|
||||
* @see #publishProgress(Object[])
|
||||
*/
|
||||
public Result doInBackground(@SuppressWarnings("unused") Params... params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on the UI thread before {@link #doInBackground(Object[])}.
|
||||
*
|
||||
* @see #end(Object)
|
||||
* @see #doInBackground(Object[])
|
||||
*/
|
||||
public void begin() {
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on the UI thread after {@link #doInBackground(Object[])}. The
|
||||
* specified result is the value returned by {@link #doInBackground(Object[])}
|
||||
* or null if the task was cancelled or an exception occured.
|
||||
*
|
||||
* @see #begin()
|
||||
* @see #doInBackground(Object[])
|
||||
*/
|
||||
public void end(@SuppressWarnings("unused") Result result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on the UI thread after {@link #publishProgress(Object[])} is invoked.
|
||||
* The specified values are the values passed to {@link #publishProgress(Object[])}.
|
||||
*
|
||||
* @see #publishProgress(Object[])
|
||||
* @see #doInBackground(Object[])
|
||||
*/
|
||||
public void processProgress(@SuppressWarnings("unused") Progress... values) {
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if this task was cancelled before it completed
|
||||
* normally.
|
||||
*
|
||||
* @return <tt>true</tt> if task was cancelled before it completed
|
||||
*
|
||||
* @see #cancel(boolean)
|
||||
*/
|
||||
public final boolean isCancelled() {
|
||||
return mFuture.isCancelled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to cancel execution of this task. This attempt will
|
||||
* fail if the task has already completed, already been cancelled,
|
||||
* or could not be cancelled for some other reason. If successful,
|
||||
* and this task has not started when <tt>cancel</tt> is called,
|
||||
* this task should never run. If the task has already started,
|
||||
* then the <tt>mayInterruptIfRunning</tt> parameter determines
|
||||
* whether the thread executing this task should be interrupted in
|
||||
* an attempt to stop the task.
|
||||
*
|
||||
* @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
|
||||
* task should be interrupted; otherwise, in-progress tasks are allowed
|
||||
* to complete.
|
||||
*
|
||||
* @return <tt>false</tt> if the task could not be cancelled,
|
||||
* typically because it has already completed normally;
|
||||
* <tt>true</tt> otherwise
|
||||
*
|
||||
* @see #isCancelled()
|
||||
*/
|
||||
public final boolean cancel(boolean mayInterruptIfRunning) {
|
||||
return mFuture.cancel(mayInterruptIfRunning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits if necessary for the computation to complete, and then
|
||||
* retrieves its result.
|
||||
*
|
||||
* @return The computed result.
|
||||
*
|
||||
* @throws CancellationException If the computation was cancelled.
|
||||
* @throws ExecutionException If the computation threw an exception.
|
||||
* @throws InterruptedException If the current thread was interrupted
|
||||
* while waiting.
|
||||
*/
|
||||
public final Result get() throws InterruptedException, ExecutionException {
|
||||
return mFuture.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits if necessary for at most the given time for the computation
|
||||
* to complete, and then retrieves its result.
|
||||
*
|
||||
* @return The computed result.
|
||||
*
|
||||
* @throws CancellationException If the computation was cancelled.
|
||||
* @throws ExecutionException If the computation threw an exception.
|
||||
* @throws InterruptedException If the current thread was interrupted
|
||||
* while waiting.
|
||||
* @throws TimeoutException If the wait timed out.
|
||||
*/
|
||||
public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
|
||||
ExecutionException, TimeoutException {
|
||||
return mFuture.get(timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the task with the specified parameters. The task returns
|
||||
* itself (this) so that the caller can keep a reference to it.
|
||||
*
|
||||
* This method must be invoked on the UI thread.
|
||||
*
|
||||
* @params params The parameters of the task.
|
||||
*
|
||||
* @return This instance of UserTask.
|
||||
*
|
||||
* @throws IllegalStateException If {@link #getStatus()} returns either
|
||||
* {@link com.google.android.photostream.UserTask.Status#RUNNING} or
|
||||
* {@link com.google.android.photostream.UserTask.Status#FINISHED}.
|
||||
*/
|
||||
public final UserTask<Params, Progress, Result> execute(Params... params) {
|
||||
if (mStatus != Status.PENDING) {
|
||||
switch (mStatus) {
|
||||
case RUNNING:
|
||||
throw new IllegalStateException("Cannot execute task:"
|
||||
+ " the task is already running.");
|
||||
case FINISHED:
|
||||
throw new IllegalStateException("Cannot execute task:"
|
||||
+ " the task has already been executed "
|
||||
+ "(a task can be executed only once)");
|
||||
}
|
||||
}
|
||||
|
||||
mStatus = Status.RUNNING;
|
||||
|
||||
begin();
|
||||
|
||||
mWorker.mParams = params;
|
||||
|
||||
try {
|
||||
sExecutor.execute(mFuture);
|
||||
} catch (RejectedExecutionException e) {
|
||||
// cannot schedule because of some other error. just die quietly
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method can be invoked from {@link #doInBackground(Object[])} to
|
||||
* publish updates on the UI thread while the background computation is
|
||||
* still running. Each call to this method will trigger the execution of
|
||||
* {@link #processProgress(Object[])} on the UI thread.
|
||||
*
|
||||
* @params values The progress values to update the UI with.
|
||||
*
|
||||
* @see #processProgress(Object[])
|
||||
* @see #doInBackground(Object[])
|
||||
*/
|
||||
protected final void publishProgress(Progress... values) {
|
||||
sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
|
||||
new UserTaskResult<Progress>(this, values)).sendToTarget();
|
||||
}
|
||||
|
||||
protected void finish(Result result) {
|
||||
end(result);
|
||||
mStatus = Status.FINISHED;
|
||||
}
|
||||
|
||||
protected static class InternalHandler extends Handler {
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
UserTaskResult result = (UserTaskResult) msg.obj;
|
||||
switch (msg.what) {
|
||||
case MESSAGE_POST_RESULT:
|
||||
// There is only one result
|
||||
result.mTask.finish(result.mData[0]);
|
||||
break;
|
||||
case MESSAGE_POST_PROGRESS:
|
||||
result.mTask.processProgress(result.mData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
|
||||
Params[] mParams;
|
||||
}
|
||||
|
||||
protected static class UserTaskResult<Data> {
|
||||
final UserTask<?, ?, ?> mTask;
|
||||
final Data[] mData;
|
||||
|
||||
UserTaskResult(UserTask<?, ?, ?> task, Data... data) {
|
||||
mTask = task;
|
||||
mData = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* ASTRID: Android's Simple Task Recording Dashboard
|
||||
*
|
||||
* Copyright (c) 2009 Tim Su
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package com.todoroo.andlib.widget;
|
||||
|
||||
import java.text.Format;
|
||||
import java.util.Date;
|
||||
|
||||
import android.app.DatePickerDialog;
|
||||
import android.app.TimePickerDialog;
|
||||
import android.app.DatePickerDialog.OnDateSetListener;
|
||||
import android.app.TimePickerDialog.OnTimeSetListener;
|
||||
import android.content.Context;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
public class DateControlSet implements OnTimeSetListener,
|
||||
OnDateSetListener, View.OnClickListener {
|
||||
|
||||
private final Format dateFormatter;
|
||||
private final Format timeFormatter;
|
||||
|
||||
protected final Context context;
|
||||
protected Button dateButton;
|
||||
protected Button timeButton;
|
||||
protected Date date;
|
||||
|
||||
protected DateControlSet(Context context) {
|
||||
this.context = context;
|
||||
|
||||
dateFormatter = DateFormat.getDateFormat(context);
|
||||
timeFormatter = DateFormat.getTimeFormat(context);
|
||||
}
|
||||
|
||||
public DateControlSet(Context context, Button dateButton, Button timeButton) {
|
||||
this(context);
|
||||
|
||||
this.dateButton = dateButton;
|
||||
this.timeButton = timeButton;
|
||||
|
||||
if(dateButton != null)
|
||||
dateButton.setOnClickListener(this);
|
||||
|
||||
if(timeButton != null)
|
||||
timeButton.setOnClickListener(this);
|
||||
|
||||
setDate(null);
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
/** Initialize the components for the given date field */
|
||||
public void setDate(Date newDate) {
|
||||
if(newDate == null) {
|
||||
date = new Date();
|
||||
} else {
|
||||
this.date = new Date(newDate.getTime());
|
||||
}
|
||||
|
||||
updateDate();
|
||||
updateTime();
|
||||
}
|
||||
|
||||
public void onDateSet(DatePicker view, int year, int month, int monthDay) {
|
||||
date.setYear(year - 1900);
|
||||
date.setMonth(month);
|
||||
date.setDate(monthDay);
|
||||
updateDate();
|
||||
}
|
||||
|
||||
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
|
||||
date.setHours(hourOfDay);
|
||||
date.setMinutes(minute);
|
||||
updateTime();
|
||||
}
|
||||
|
||||
public void updateDate() {
|
||||
if(dateButton != null)
|
||||
dateButton.setText(dateFormatter.format(date));
|
||||
|
||||
}
|
||||
|
||||
public void updateTime() {
|
||||
if(timeButton != null)
|
||||
timeButton.setText(timeFormatter.format(date));
|
||||
}
|
||||
|
||||
public void onClick(View v) {
|
||||
if(v == timeButton)
|
||||
new TimePickerDialog(context, this, date.getHours(),
|
||||
date.getMinutes(), false).show();
|
||||
else
|
||||
new DatePickerDialog(context, this, 1900 +
|
||||
date.getYear(), date.getMonth(), date.getDate()).show();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* ASTRID: Android's Simple Task Recording Dashboard
|
||||
*
|
||||
* Copyright (c) 2009 Tim Su
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package com.todoroo.andlib.widget;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
/** Date Control Set with an "enabled" checkbox" to toggle date / null */
|
||||
public class DateWithNullControlSet extends DateControlSet {
|
||||
|
||||
private CheckBox activatedCheckBox;
|
||||
|
||||
public DateWithNullControlSet(Activity activity, int checkBoxId, int dateButtonId, int timeButtonId) {
|
||||
super(activity);
|
||||
activatedCheckBox = (CheckBox)activity.findViewById(checkBoxId);
|
||||
dateButton = (Button)activity.findViewById(dateButtonId);
|
||||
timeButton = (Button)activity.findViewById(timeButtonId);
|
||||
|
||||
activatedCheckBox.setOnCheckedChangeListener(
|
||||
new OnCheckedChangeListener() {
|
||||
public void onCheckedChanged(CompoundButton buttonView,
|
||||
boolean isChecked) {
|
||||
dateButton.setEnabled(isChecked);
|
||||
timeButton.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
dateButton.setOnClickListener(this);
|
||||
timeButton.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getDate() {
|
||||
if(!activatedCheckBox.isChecked())
|
||||
return null;
|
||||
return super.getDate();
|
||||
}
|
||||
|
||||
/** Initialize the components for the given date field */
|
||||
@Override
|
||||
public void setDate(Date newDate) {
|
||||
activatedCheckBox.setChecked(newDate != null);
|
||||
dateButton.setEnabled(newDate != null);
|
||||
timeButton.setEnabled(newDate != null);
|
||||
|
||||
super.setDate(newDate);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* ASTRID: Android's Simple Task Recording Dashboard
|
||||
*
|
||||
* Copyright (c) 2009 Tim Su
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package com.todoroo.andlib.widget;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.FrameLayout.LayoutParams;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
|
||||
/** Dialog box with an arbitrary number of number pickers */
|
||||
public class NNumberPickerDialog extends AlertDialog implements OnClickListener {
|
||||
|
||||
@Autowired
|
||||
private Integer nNumberPickerLayout;
|
||||
|
||||
public interface OnNNumberPickedListener {
|
||||
void onNumbersPicked(int[] number);
|
||||
}
|
||||
|
||||
private final List<NumberPickerWidget> pickers = new LinkedList<NumberPickerWidget>();
|
||||
private final OnNNumberPickedListener mCallback;
|
||||
|
||||
/** Instantiate the dialog box.
|
||||
*
|
||||
* @param context
|
||||
* @param callBack callback function to get the numbers you requested
|
||||
* @param title title of the dialog box
|
||||
* @param initialValue initial picker values array
|
||||
* @param incrementBy picker increment by array
|
||||
* @param start picker range start array
|
||||
* @param end picker range end array
|
||||
* @param separators text separating the spinners. whole array, or individual
|
||||
* elements can be null
|
||||
*/
|
||||
public NNumberPickerDialog(Context context, OnNNumberPickedListener callBack,
|
||||
String title, int[] initialValue, int[] incrementBy, int[] start,
|
||||
int[] end, String[] separators) {
|
||||
super(context);
|
||||
mCallback = callBack;
|
||||
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
|
||||
setButton(context.getText(android.R.string.ok), this);
|
||||
setButton2(context.getText(android.R.string.cancel), (OnClickListener) null);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View view = inflater.inflate(nNumberPickerLayout, null);
|
||||
setView(view);
|
||||
LinearLayout container = (LinearLayout)view;
|
||||
|
||||
setTitle(title);
|
||||
LayoutParams npLayout = new LayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.FILL_PARENT);
|
||||
npLayout.gravity = 1;
|
||||
LayoutParams sepLayout = new LayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.FILL_PARENT);
|
||||
for(int i = 0; i < incrementBy.length; i++) {
|
||||
NumberPickerWidget np = new NumberPickerWidget(context, null);
|
||||
np.setIncrementBy(incrementBy[i]);
|
||||
np.setLayoutParams(npLayout);
|
||||
np.setRange(start[i], end[i]);
|
||||
np.setCurrent(initialValue[i]);
|
||||
|
||||
container.addView(np);
|
||||
pickers.add(np);
|
||||
|
||||
if(separators != null && separators[i] != null) {
|
||||
TextView text = new TextView(context);
|
||||
text.setText(separators[i]);
|
||||
if(separators[i].length() < 3)
|
||||
text.setTextSize(48);
|
||||
else
|
||||
text.setTextSize(20);
|
||||
text.setGravity(Gravity.CENTER);
|
||||
text.setLayoutParams(sepLayout);
|
||||
container.addView(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setInitialValues(int[] values) {
|
||||
for(int i = 0; i < pickers.size(); i++)
|
||||
pickers.get(i).setCurrent(values[i]);
|
||||
}
|
||||
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (mCallback != null) {
|
||||
int[] values = new int[pickers.size()];
|
||||
for(int i = 0; i < pickers.size(); i++) {
|
||||
pickers.get(i).clearFocus();
|
||||
values[i] = pickers.get(i).getCurrent();
|
||||
}
|
||||
mCallback.onNumbersPicked(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* ASTRID: Android's Simple Task Recording Dashboard
|
||||
*
|
||||
* Copyright (c) 2009 Tim Su
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package com.todoroo.andlib.widget;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
|
||||
public class NumberPickerDialog extends AlertDialog implements OnClickListener {
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerDialogLayout;
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerId;
|
||||
|
||||
public interface OnNumberPickedListener {
|
||||
void onNumberPicked(NumberPickerWidget view, int number);
|
||||
}
|
||||
|
||||
private final NumberPickerWidget mPicker;
|
||||
private final OnNumberPickedListener mCallback;
|
||||
|
||||
public NumberPickerDialog(Context context, OnNumberPickedListener callBack,
|
||||
String title, int initialValue, int incrementBy, int start, int end) {
|
||||
super(context);
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
|
||||
mCallback = callBack;
|
||||
|
||||
setButton(context.getText(android.R.string.ok), this);
|
||||
setButton2(context.getText(android.R.string.cancel), (OnClickListener) null);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View view = inflater.inflate(numberPickerDialogLayout, null);
|
||||
setView(view);
|
||||
|
||||
setTitle(title);
|
||||
mPicker = (NumberPickerWidget) view.findViewById(numberPickerId);
|
||||
mPicker.setIncrementBy(incrementBy);
|
||||
mPicker.setRange(start, end);
|
||||
mPicker.setCurrent(initialValue);
|
||||
}
|
||||
|
||||
public void setInitialValue(int initialValue) {
|
||||
mPicker.setCurrent(initialValue);
|
||||
}
|
||||
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (mCallback != null) {
|
||||
mPicker.clearFocus();
|
||||
mCallback.onNumberPicked(mPicker, mPicker.getCurrent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,460 @@
|
||||
/*
|
||||
* ASTRID: Android's Simple Task Recording Dashboard
|
||||
*
|
||||
* Copyright (c) 2009 Tim Su
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package com.todoroo.andlib.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.NumberKeyListener;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnFocusChangeListener;
|
||||
import android.view.View.OnLongClickListener;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
|
||||
public class NumberPickerWidget extends LinearLayout implements OnClickListener,
|
||||
OnFocusChangeListener, OnLongClickListener {
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerLayout;
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerIncrementId;
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerDecrementId;
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerInputId;
|
||||
|
||||
public interface OnChangedListener {
|
||||
void onChanged(NumberPickerWidget picker, int oldVal, int newVal);
|
||||
}
|
||||
|
||||
public interface Formatter {
|
||||
String toString(int value);
|
||||
}
|
||||
|
||||
/*
|
||||
* Use a custom NumberPicker formatting callback to use two-digit minutes
|
||||
* strings like "01". Keeping a static formatter etc. is the most efficient
|
||||
* way to do this; it avoids creating temporary objects on every call to
|
||||
* format().
|
||||
*/
|
||||
public static final NumberPickerWidget.Formatter TWO_DIGIT_FORMATTER = new NumberPickerWidget.Formatter() {
|
||||
final StringBuilder mBuilder = new StringBuilder();
|
||||
final java.util.Formatter mFmt = new java.util.Formatter(mBuilder);
|
||||
final Object[] mArgs = new Object[1];
|
||||
|
||||
public String toString(int value) {
|
||||
mArgs[0] = value;
|
||||
mBuilder.delete(0, mBuilder.length());
|
||||
mFmt.format("%02d", mArgs); //$NON-NLS-1$
|
||||
return mFmt.toString();
|
||||
}
|
||||
};
|
||||
|
||||
protected int incrementBy = 1;
|
||||
public void setIncrementBy(int incrementBy) {
|
||||
this.incrementBy = incrementBy;
|
||||
}
|
||||
|
||||
protected final Handler mHandler;
|
||||
private final Runnable mRunnable = new Runnable() {
|
||||
public void run() {
|
||||
if (mIncrement) {
|
||||
changeCurrent(mCurrent + incrementBy, mSlideUpInAnimation, mSlideUpOutAnimation);
|
||||
mHandler.postDelayed(this, mSpeed);
|
||||
} else if (mDecrement) {
|
||||
changeCurrent(mCurrent - incrementBy, mSlideDownInAnimation, mSlideDownOutAnimation);
|
||||
mHandler.postDelayed(this, mSpeed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final LayoutInflater mInflater;
|
||||
private final TextView mText;
|
||||
protected final InputFilter mInputFilter;
|
||||
protected final InputFilter mNumberInputFilter;
|
||||
|
||||
protected final Animation mSlideUpOutAnimation;
|
||||
protected final Animation mSlideUpInAnimation;
|
||||
protected final Animation mSlideDownOutAnimation;
|
||||
protected final Animation mSlideDownInAnimation;
|
||||
|
||||
protected String[] mDisplayedValues;
|
||||
protected int mStart;
|
||||
protected int mEnd;
|
||||
protected int mCurrent;
|
||||
protected int mPrevious;
|
||||
private OnChangedListener mListener;
|
||||
private Formatter mFormatter;
|
||||
protected long mSpeed = 500;
|
||||
|
||||
protected boolean mIncrement;
|
||||
protected boolean mDecrement;
|
||||
|
||||
public NumberPickerWidget(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public NumberPickerWidget(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
|
||||
setOrientation(VERTICAL);
|
||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mInflater.inflate(numberPickerLayout, this, true);
|
||||
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mInputFilter = new NumberPickerInputFilter();
|
||||
mNumberInputFilter = new NumberRangeKeyListener();
|
||||
mIncrementButton = (NumberPickerWidgetButton) findViewById(numberPickerIncrementId);
|
||||
mIncrementButton.setOnClickListener(this);
|
||||
mIncrementButton.setOnLongClickListener(this);
|
||||
mIncrementButton.setNumberPicker(this);
|
||||
mDecrementButton = (NumberPickerWidgetButton) findViewById(numberPickerDecrementId);
|
||||
mDecrementButton.setOnClickListener(this);
|
||||
mDecrementButton.setOnLongClickListener(this);
|
||||
mDecrementButton.setNumberPicker(this);
|
||||
|
||||
mText = (TextView) findViewById(numberPickerInputId);
|
||||
mText.setOnFocusChangeListener(this);
|
||||
mText.setFilters(new InputFilter[] { mInputFilter });
|
||||
|
||||
mSlideUpOutAnimation = new TranslateAnimation(
|
||||
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
|
||||
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, -100);
|
||||
mSlideUpOutAnimation.setDuration(200);
|
||||
mSlideUpInAnimation = new TranslateAnimation(
|
||||
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
|
||||
Animation.RELATIVE_TO_SELF, 100, Animation.RELATIVE_TO_SELF, 0);
|
||||
mSlideUpInAnimation.setDuration(200);
|
||||
mSlideDownOutAnimation = new TranslateAnimation(
|
||||
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
|
||||
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 100);
|
||||
mSlideDownOutAnimation.setDuration(200);
|
||||
mSlideDownInAnimation = new TranslateAnimation(
|
||||
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
|
||||
Animation.RELATIVE_TO_SELF, -100, Animation.RELATIVE_TO_SELF, 0);
|
||||
mSlideDownInAnimation.setDuration(200);
|
||||
|
||||
if (!isEnabled()) {
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
mIncrementButton.setEnabled(enabled);
|
||||
mDecrementButton.setEnabled(enabled);
|
||||
mText.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setOnChangeListener(OnChangedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void setFormatter(Formatter formatter) {
|
||||
mFormatter = formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the range of numbers allowed for the number picker. The current value
|
||||
* will be automatically set to the start.
|
||||
*
|
||||
* @param start
|
||||
* the start of the range (inclusive)
|
||||
* @param end
|
||||
* the end of the range (inclusive)
|
||||
*/
|
||||
public void setRange(int start, int end) {
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
mCurrent = start;
|
||||
updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the range of numbers allowed for the number picker. The current value
|
||||
* will be automatically set to the start. Also provide a mapping for values
|
||||
* used to display to the user.
|
||||
*
|
||||
* @param start
|
||||
* the start of the range (inclusive)
|
||||
* @param end
|
||||
* the end of the range (inclusive)
|
||||
* @param displayedValues
|
||||
* the values displayed to the user.
|
||||
*/
|
||||
public void setRange(int start, int end, String[] displayedValues) {
|
||||
mDisplayedValues = displayedValues;
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
mCurrent = start;
|
||||
updateView();
|
||||
}
|
||||
|
||||
public void setCurrent(int current) {
|
||||
mCurrent = current;
|
||||
updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* The speed (in milliseconds) at which the numbers will scroll when the the
|
||||
* +/- buttons are longpressed. Default is 300ms.
|
||||
*/
|
||||
public void setSpeed(long speed) {
|
||||
mSpeed = speed;
|
||||
}
|
||||
|
||||
public void onClick(View v) {
|
||||
|
||||
/*
|
||||
* The text view may still have focus so clear it's focus which will
|
||||
* trigger the on focus changed and any typed values to be pulled.
|
||||
*/
|
||||
mText.clearFocus();
|
||||
|
||||
// now perform the increment/decrement
|
||||
if (numberPickerIncrementId == v.getId()) {
|
||||
changeCurrent(mCurrent + incrementBy, mSlideUpInAnimation,
|
||||
mSlideUpOutAnimation);
|
||||
} else if (numberPickerDecrementId == v.getId()) {
|
||||
changeCurrent(mCurrent - incrementBy, mSlideDownInAnimation,
|
||||
mSlideDownOutAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
private String formatNumber(int value) {
|
||||
return (mFormatter != null) ? mFormatter.toString(value)
|
||||
: String.valueOf(value);
|
||||
}
|
||||
|
||||
protected void changeCurrent(int current,
|
||||
@SuppressWarnings("unused") Animation in,
|
||||
@SuppressWarnings("unused") Animation out) {
|
||||
|
||||
// Wrap around the values if we go past the start or end
|
||||
if (current > mEnd) {
|
||||
current = mStart;
|
||||
} else if (current < mStart) {
|
||||
current = mEnd;
|
||||
}
|
||||
mPrevious = mCurrent;
|
||||
mCurrent = current;
|
||||
notifyChange();
|
||||
updateView();
|
||||
}
|
||||
|
||||
private void notifyChange() {
|
||||
if (mListener != null) {
|
||||
mListener.onChanged(this, mPrevious, mCurrent);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateView() {
|
||||
|
||||
/*
|
||||
* If we don't have displayed values then use the current number else
|
||||
* find the correct value in the displayed values for the current
|
||||
* number.
|
||||
*/
|
||||
if (mDisplayedValues == null) {
|
||||
mText.setText(formatNumber(mCurrent));
|
||||
} else {
|
||||
mText.setText(mDisplayedValues[mCurrent - mStart]);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCurrentView(CharSequence str) {
|
||||
int val = getSelectedPos(str.toString());
|
||||
if ((val >= mStart) && (val <= mEnd)) {
|
||||
mPrevious = mCurrent;
|
||||
mCurrent = val;
|
||||
notifyChange();
|
||||
}
|
||||
updateView();
|
||||
}
|
||||
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
|
||||
/*
|
||||
* When focus is lost check that the text field has valid values.
|
||||
*/
|
||||
if (!hasFocus && v instanceof TextView) {
|
||||
String str = String.valueOf(((TextView) v).getText());
|
||||
if ("".equals(str)) { //$NON-NLS-1$
|
||||
|
||||
// Restore to the old value as we don't allow empty values
|
||||
updateView();
|
||||
} else {
|
||||
|
||||
// Check the new value and ensure it's in range
|
||||
validateCurrentView(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We start the long click here but rely on the {@link NumberPickerWidgetButton}
|
||||
* to inform us when the long click has ended.
|
||||
*/
|
||||
public boolean onLongClick(View v) {
|
||||
|
||||
/*
|
||||
* The text view may still have focus so clear it's focus which will
|
||||
* trigger the on focus changed and any typed values to be pulled.
|
||||
*/
|
||||
mText.clearFocus();
|
||||
|
||||
if (numberPickerIncrementId == v.getId()) {
|
||||
mIncrement = true;
|
||||
mHandler.post(mRunnable);
|
||||
} else if (numberPickerDecrementId == v.getId()) {
|
||||
mDecrement = true;
|
||||
mHandler.post(mRunnable);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void cancelIncrement() {
|
||||
mIncrement = false;
|
||||
}
|
||||
|
||||
public void cancelDecrement() {
|
||||
mDecrement = false;
|
||||
}
|
||||
|
||||
protected static final char[] DIGIT_CHARACTERS = new char[] { '0', '1', '2',
|
||||
'3', '4', '5', '6', '7', '8', '9' };
|
||||
|
||||
private NumberPickerWidgetButton mIncrementButton;
|
||||
private NumberPickerWidgetButton mDecrementButton;
|
||||
|
||||
class NumberPickerInputFilter implements InputFilter {
|
||||
public CharSequence filter(CharSequence source, int start, int end,
|
||||
Spanned dest, int dstart, int dend) {
|
||||
if (mDisplayedValues == null) {
|
||||
return mNumberInputFilter.filter(source, start, end, dest,
|
||||
dstart, dend);
|
||||
}
|
||||
CharSequence filtered = String.valueOf(source.subSequence(start,
|
||||
end));
|
||||
String result = String.valueOf(dest.subSequence(0, dstart))
|
||||
+ filtered + dest.subSequence(dend, dest.length());
|
||||
String str = String.valueOf(result).toLowerCase();
|
||||
for (String val : mDisplayedValues) {
|
||||
val = val.toLowerCase();
|
||||
if (val.startsWith(str)) {
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
return ""; //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
|
||||
class NumberRangeKeyListener extends NumberKeyListener {
|
||||
|
||||
@Override
|
||||
protected char[] getAcceptedChars() {
|
||||
return DIGIT_CHARACTERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end,
|
||||
Spanned dest, int dstart, int dend) {
|
||||
|
||||
CharSequence filtered = super.filter(source, start, end, dest,
|
||||
dstart, dend);
|
||||
if (filtered == null) {
|
||||
filtered = source.subSequence(start, end);
|
||||
}
|
||||
|
||||
String result = String.valueOf(dest.subSequence(0, dstart))
|
||||
+ filtered + dest.subSequence(dend, dest.length());
|
||||
|
||||
if ("".equals(result)) { //$NON-NLS-1$
|
||||
return result;
|
||||
}
|
||||
int val = getSelectedPos(result);
|
||||
|
||||
/*
|
||||
* Ensure the user can't type in a value greater than the max
|
||||
* allowed. We have to allow less than min as the user might want to
|
||||
* delete some numbers and then type a new number.
|
||||
*/
|
||||
if (val > mEnd) {
|
||||
return ""; //$NON-NLS-1$
|
||||
} else {
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
||||
public int getInputType() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected int getSelectedPos(String str) {
|
||||
if (mDisplayedValues == null) {
|
||||
return Integer.parseInt(str);
|
||||
} else {
|
||||
for (int i = 0; i < mDisplayedValues.length; i++) {
|
||||
|
||||
/* Don't force the user to type in jan when ja will do */
|
||||
str = str.toLowerCase();
|
||||
if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
|
||||
return mStart + i;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The user might have typed in a number into the month field i.e.
|
||||
* 10 instead of OCT so support that too.
|
||||
*/
|
||||
try {
|
||||
return Integer.parseInt(str);
|
||||
} catch (NumberFormatException e) {
|
||||
|
||||
/* Ignore as if it's not a number we don't care */
|
||||
}
|
||||
}
|
||||
return mStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current value.
|
||||
*/
|
||||
public int getCurrent() {
|
||||
return mCurrent;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* ASTRID: Android's Simple Task Recording Dashboard
|
||||
*
|
||||
* Copyright (c) 2009 Tim Su
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package com.todoroo.andlib.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
|
||||
/**
|
||||
* This class exists purely to cancel long click events.
|
||||
*/
|
||||
public class NumberPickerWidgetButton extends ImageButton {
|
||||
|
||||
private NumberPickerWidget mNumberPicker;
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerIncrementId;
|
||||
|
||||
@Autowired
|
||||
private Integer numberPickerDecrementId;
|
||||
|
||||
public NumberPickerWidgetButton(Context context, AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
public NumberPickerWidgetButton(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
public NumberPickerWidgetButton(Context context) {
|
||||
super(context);
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
public void setNumberPicker(NumberPickerWidget picker) {
|
||||
mNumberPicker = picker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
cancelLongpressIfRequired(event);
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTrackballEvent(MotionEvent event) {
|
||||
cancelLongpressIfRequired(event);
|
||||
return super.onTrackballEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER)
|
||||
|| (keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
cancelLongpress();
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
private void cancelLongpressIfRequired(MotionEvent event) {
|
||||
if ((event.getAction() == MotionEvent.ACTION_CANCEL)
|
||||
|| (event.getAction() == MotionEvent.ACTION_UP)) {
|
||||
cancelLongpress();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelLongpress() {
|
||||
if (numberPickerIncrementId == getId()) {
|
||||
mNumberPicker.cancelIncrement();
|
||||
} else if (numberPickerDecrementId == getId()) {
|
||||
mNumberPicker.cancelDecrement();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue