Re-add submodules as normal files

pull/14/head
Tim Su 16 years ago
parent 09928c5cf6
commit f61ff9a71d

@ -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&lt;URL, Integer, Long&gt; {
* 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…
Cancel
Save