diff --git a/common-src/com/todoroo/andlib/data/AbstractDatabase.java b/common-src/com/todoroo/andlib/data/AbstractDatabase.java
new file mode 100644
index 000000000..ac68b5e5c
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/AbstractDatabase.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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
+ *
+ */
+@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
+ *
+ */
+ public static class SqlConstructorVisitor implements PropertyVisitor {
+
+ public String visitDouble(Property property, Void data) {
+ return String.format("%s REAL", property.name);
+ }
+
+ public String visitInteger(Property property, Void data) {
+ return String.format("%s INTEGER", property.name);
+ }
+
+ public String visitLong(Property property, Void data) {
+ return String.format("%s INTEGER", property.name);
+ }
+
+ public String visitString(Property property, Void data) {
+ return String.format("%s TEXT", property.name);
+ }
+ }
+}
+
diff --git a/common-src/com/todoroo/andlib/data/AbstractModel.java b/common-src/com/todoroo/andlib/data/AbstractModel.java
new file mode 100644
index 000000000..61682c9e1
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/AbstractModel.java
@@ -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;
+
+/**
+ * AbstractModel represents a row in a database.
+ *
+ * A single database can be represented by multiple AbstractModels
+ * corresponding to different queries that return a different set of columns.
+ * Each model exposes a set of properties that it contains.
+ *
+ * @author Tim Su
+ *
+ */
+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 getValue(Property 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 boolean shouldSaveValue(
+ Property 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 void setValue(Property 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> properties = new ArrayList>();
+ 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
+ *
+ */
+ public static class ContentValuesSavingVisitor implements PropertyVisitor {
+
+ private ContentValues store;
+
+ public synchronized void save(Property> property, ContentValues newStore, Object value) {
+ this.store = newStore;
+ property.accept(this, value);
+ }
+
+ public Void visitDouble(Property property, Object value) {
+ store.put(property.name, (Double) value);
+ return null;
+ }
+
+ public Void visitInteger(Property property, Object value) {
+ store.put(property.name, (Integer) value);
+ return null;
+ }
+
+ public Void visitLong(Property property, Object value) {
+ store.put(property.name, (Long) value);
+ return null;
+ }
+
+ public Void visitString(Property 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
+ implements Parcelable.Creator {
+
+ private Class cls;
+
+ public ModelCreator(Class 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);
+ };
+ };
+
+}
diff --git a/common-src/com/todoroo/andlib/data/GenericDao.java b/common-src/com/todoroo/andlib/data/GenericDao.java
new file mode 100644
index 000000000..7fe7b38f5
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/GenericDao.java
@@ -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
+ *
+ */
+public class GenericDao {
+
+ private Class modelClass;
+
+ private Table table;
+
+ private AbstractDatabase database;
+
+ public GenericDao(Class modelClass) {
+ this.modelClass = modelClass;
+ }
+
+ public GenericDao(Class 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 query(Query query) {
+ query.from(table);
+ Cursor cursor = database.getDatabase().rawQuery(query.toString(), null);
+ return new TodorooCursor(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 cursor = fetchItem(id, properties);
+ try {
+ if (cursor.getCount() == 0)
+ return null;
+ Constructor 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 fetchItem(long id, Property>... properties) {
+ TodorooCursor cursor = query(
+ Query.select(properties).where(AbstractModel.ID_PROPERTY.eq(id)));
+ cursor.moveToFirst();
+ return new TodorooCursor(cursor, properties);
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/Property.java b/common-src/com/todoroo/andlib/data/Property.java
new file mode 100644
index 000000000..6c87373ae
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/Property.java
@@ -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
+ *
+ * @param
+ * a database supported type, such as String or Integer
+ */
+@SuppressWarnings("nls")
+public abstract class Property 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 accept(
+ PropertyVisitor visitor, PARAMETER data);
+
+ /**
+ * Return a clone of this property
+ */
+ @Override
+ public Property clone() {
+ try {
+ return (Property) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // --- helper classes and interfaces
+
+ /**
+ * Visitor interface for property classes
+ *
+ * @author Tim Su
+ *
+ */
+ public interface PropertyVisitor {
+ public RETURN visitInteger(Property property, PARAMETER data);
+
+ public RETURN visitLong(Property property, PARAMETER data);
+
+ public RETURN visitDouble(Property property, PARAMETER data);
+
+ public RETURN visitString(Property property, PARAMETER data);
+ }
+
+ // --- children
+
+ /**
+ * Integer property type. See {@link Property}
+ *
+ * @author Tim Su
+ *
+ */
+ public static class IntegerProperty extends Property {
+
+ public IntegerProperty(Table table, String name) {
+ super(table, name);
+ }
+
+ protected IntegerProperty(Table table, String name, String expression) {
+ super(table, name, expression);
+ }
+
+ @Override
+ public RETURN accept(
+ PropertyVisitor visitor, PARAMETER data) {
+ return visitor.visitInteger(this, data);
+ }
+ }
+
+ /**
+ * String property type. See {@link Property}
+ *
+ * @author Tim Su
+ *
+ */
+ public static class StringProperty extends Property {
+
+ public StringProperty(Table table, String name) {
+ super(table, name);
+ }
+
+ protected StringProperty(Table table, String name, String expression) {
+ super(table, name, expression);
+ }
+
+ @Override
+ public RETURN accept(
+ PropertyVisitor visitor, PARAMETER data) {
+ return visitor.visitString(this, data);
+ }
+ }
+
+ /**
+ * Double property type. See {@link Property}
+ *
+ * @author Tim Su
+ *
+ */
+ public static class DoubleProperty extends Property {
+
+ public DoubleProperty(Table table, String name) {
+ super(table, name);
+ }
+
+ protected DoubleProperty(Table table, String name, String expression) {
+ super(table, name, expression);
+ }
+
+
+ @Override
+ public RETURN accept(
+ PropertyVisitor visitor, PARAMETER data) {
+ return visitor.visitDouble(this, data);
+ }
+ }
+
+ /**
+ * Long property type. See {@link Property}
+ *
+ * @author Tim Su
+ *
+ */
+ public static class LongProperty extends Property {
+
+ public LongProperty(Table table, String name) {
+ super(table, name);
+ }
+
+ protected LongProperty(Table table, String name, String expression) {
+ super(table, name, expression);
+ }
+
+ @Override
+ public RETURN accept(
+ PropertyVisitor 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");
+ }
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/data/Table.java b/common-src/com/todoroo/andlib/data/Table.java
new file mode 100644
index 000000000..ae8d59274
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/Table.java
@@ -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 as will
+ * clone the table when it returns.
+ *
+ * @author Tim Su
+ *
+ */
+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);
+ }
+}
\ No newline at end of file
diff --git a/common-src/com/todoroo/andlib/data/TodorooCursor.java b/common-src/com/todoroo/andlib/data/TodorooCursor.java
new file mode 100644
index 000000000..4ef2c8ccc
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/TodorooCursor.java
@@ -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
+ *
+ * @param a model type that is returned by this cursor
+ */
+public class TodorooCursor 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 columnIndexCache;
+
+ /** Property reading visitor */
+ private static final CursorReadingVisitor reader = new CursorReadingVisitor();
+
+ /**
+ * Create an AstridCursor 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();
+ }
+
+ /**
+ * Get the value for the given property on the underlying {@link Cursor}
+ *
+ * @param type to return
+ * @param property to retrieve
+ * @return
+ */
+ public PROPERTY_TYPE get(Property 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
+ *
+ */
+ public static class CursorReadingVisitor implements PropertyVisitor> {
+
+ public Object visitDouble(Property property,
+ TodorooCursor> cursor) {
+ return cursor.getDouble(cursor.getColumnIndexFromCache(property.name));
+ }
+
+ public Object visitInteger(Property property,
+ TodorooCursor> cursor) {
+ return cursor.getInt(cursor.getColumnIndexFromCache(property.name));
+ }
+
+ public Object visitLong(Property property, TodorooCursor> cursor) {
+ return cursor.getLong(cursor.getColumnIndexFromCache(property.name));
+ }
+
+ public Object visitString(Property property,
+ TodorooCursor> cursor) {
+ return cursor.getString(cursor.getColumnIndexFromCache(property.name));
+ }
+
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Constants.java b/common-src/com/todoroo/andlib/data/sql/Constants.java
new file mode 100644
index 000000000..4596aa34b
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Constants.java
@@ -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";
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Criterion.java b/common-src/com/todoroo/andlib/data/sql/Criterion.java
new file mode 100644
index 000000000..bae70f44d
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Criterion.java
@@ -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();
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/DBObject.java b/common-src/com/todoroo/andlib/data/sql/DBObject.java
new file mode 100644
index 000000000..ad8e5ea83
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/DBObject.java
@@ -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> {
+ 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();
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/EqCriterion.java b/common-src/com/todoroo/andlib/data/sql/EqCriterion.java
new file mode 100644
index 000000000..b2ec34cb8
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/EqCriterion.java
@@ -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);
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Field.java b/common-src/com/todoroo/andlib/data/sql/Field.java
new file mode 100644
index 000000000..9111d9c25
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Field.java
@@ -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 {
+
+ 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 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);
+ }
+ };
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/GroupBy.java b/common-src/com/todoroo/andlib/data/sql/GroupBy.java
new file mode 100644
index 000000000..103128411
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/GroupBy.java
@@ -0,0 +1,14 @@
+package com.todoroo.andlib.data.sql;
+
+import java.util.List;
+import java.util.ArrayList;
+
+public class GroupBy {
+ private List fields = new ArrayList();
+
+ public static GroupBy groupBy(Field field) {
+ GroupBy groupBy = new GroupBy();
+ groupBy.fields.add(field);
+ return groupBy;
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Join.java b/common-src/com/todoroo/andlib/data/sql/Join.java
new file mode 100644
index 000000000..f3809dba9
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Join.java
@@ -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();
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/JoinType.java b/common-src/com/todoroo/andlib/data/sql/JoinType.java
new file mode 100644
index 000000000..86cd8f2d3
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/JoinType.java
@@ -0,0 +1,5 @@
+package com.todoroo.andlib.data.sql;
+
+public enum JoinType {
+ INNER, LEFT, RIGHT, OUT
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Operator.java b/common-src/com/todoroo/andlib/data/sql/Operator.java
new file mode 100644
index 000000000..55981e71a
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Operator.java
@@ -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 contraryRegistry = new HashMap();
+
+ 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();
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Order.java b/common-src/com/todoroo/andlib/data/sql/Order.java
new file mode 100644
index 000000000..5bff6bf49
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Order.java
@@ -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;
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/OrderType.java b/common-src/com/todoroo/andlib/data/sql/OrderType.java
new file mode 100644
index 000000000..b3b8fa5af
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/OrderType.java
@@ -0,0 +1,5 @@
+package com.todoroo.andlib.data.sql;
+
+public enum OrderType {
+ DESC, ASC
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Query.java b/common-src/com/todoroo/andlib/data/sql/Query.java
new file mode 100644
index 000000000..2d37cbf7e
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Query.java
@@ -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 criterions = new ArrayList();
+ private List> fields = new ArrayList>();
+ private List joins = new ArrayList();
+ private List groupBies = new ArrayList();
+ private List orders = new ArrayList();
+ private List havings = new ArrayList();
+
+ 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()]);
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/Table.java b/common-src/com/todoroo/andlib/data/sql/Table.java
new file mode 100644
index 000000000..ee04ae0df
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/Table.java
@@ -0,0 +1,20 @@
+package com.todoroo.andlib.data.sql;
+
+public class Table extends DBObject {
+
+ 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;
+ }
+}
diff --git a/common-src/com/todoroo/andlib/data/sql/UnaryCriterion.java b/common-src/com/todoroo/andlib/data/sql/UnaryCriterion.java
new file mode 100644
index 000000000..387c8f36d
--- /dev/null
+++ b/common-src/com/todoroo/andlib/data/sql/UnaryCriterion.java
@@ -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);
+ }
+ };
+ }
+}
diff --git a/common-src/com/todoroo/andlib/service/AbstractDependencyInjector.java b/common-src/com/todoroo/andlib/service/AbstractDependencyInjector.java
new file mode 100644
index 000000000..fbf4fdfb8
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/AbstractDependencyInjector.java
@@ -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
+ *
+ */
+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);
+
+}
diff --git a/common-src/com/todoroo/andlib/service/Autowired.java b/common-src/com/todoroo/andlib/service/Autowired.java
new file mode 100644
index 000000000..3b0b1c765
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/Autowired.java
@@ -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
+ *
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Autowired {
+ //
+}
diff --git a/common-src/com/todoroo/andlib/service/ContextManager.java b/common-src/com/todoroo/andlib/service/ContextManager.java
new file mode 100644
index 000000000..de75b7bbc
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/ContextManager.java
@@ -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
+ *
+ */
+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);
+ }
+}
diff --git a/common-src/com/todoroo/andlib/service/DependencyInjectionService.java b/common-src/com/todoroo/andlib/service/DependencyInjectionService.java
new file mode 100644
index 000000000..27b43e76f
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/DependencyInjectionService.java
@@ -0,0 +1,137 @@
+package com.todoroo.andlib.service;
+
+import java.lang.reflect.Field;
+
+/**
+ * Simple Dependency Injection Service for Android.
+ *
+ * Add dependency injectors to the injector chain, then invoke this method
+ * against classes you wish to perform dependency injection for.
+ *
+ * All errors encountered are handled as warnings, so if dependency injection
+ * seems to be failing, check the logs for more information.
+ *
+ * @author Tim Su
+ *
+ */
+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;
+ }
+}
diff --git a/common-src/com/todoroo/andlib/service/ExceptionService.java b/common-src/com/todoroo/andlib/service/ExceptionService.java
new file mode 100644
index 000000000..223655d0a
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/ExceptionService.java
@@ -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
+ *
+ */
+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
+ *
+ */
+ public interface ErrorReporter {
+ public void handleError(String name, Throwable error);
+ }
+
+ /**
+ * AndroidLogReporter reports errors to LogCat
+ *
+ * @author Tim Su
+ *
+ */
+ 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
+ *
+ */
+ 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);
+ }
+ }
+
+}
+
diff --git a/common-src/com/todoroo/andlib/service/HttpErrorException.java b/common-src/com/todoroo/andlib/service/HttpErrorException.java
new file mode 100644
index 000000000..c2c46fc03
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/HttpErrorException.java
@@ -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$
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/service/HttpRestClient.java b/common-src/com/todoroo/andlib/service/HttpRestClient.java
new file mode 100644
index 000000000..fa2b470c1
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/HttpRestClient.java
@@ -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.
+ *
+ * Portions by Praeda:
+ * http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple
+ * -restful-client-at-android/
+ *
+ * @author Tim Su
+ *
+ */
+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 = 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(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;
+ }
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/service/HttpUnavailableException.java b/common-src/com/todoroo/andlib/service/HttpUnavailableException.java
new file mode 100644
index 000000000..ac6e8c860
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/HttpUnavailableException.java
@@ -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
+ *
+ */
+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
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/service/NotificationManager.java b/common-src/com/todoroo/andlib/service/NotificationManager.java
new file mode 100644
index 000000000..cb9ff8371
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/NotificationManager.java
@@ -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);
+ }
+ }
+}
diff --git a/common-src/com/todoroo/andlib/service/RestClient.java b/common-src/com/todoroo/andlib/service/RestClient.java
new file mode 100644
index 000000000..0bf35e461
--- /dev/null
+++ b/common-src/com/todoroo/andlib/service/RestClient.java
@@ -0,0 +1,14 @@
+package com.todoroo.andlib.service;
+
+import java.io.IOException;
+
+/**
+ * RestClient stub invokes the HTML requests as desired
+ *
+ * @author Tim Su
+ *
+ */
+public interface RestClient {
+ public String get(String url) throws IOException;
+ public String post(String url, String data) throws IOException;
+}
\ No newline at end of file
diff --git a/common-src/com/todoroo/andlib/utility/AndroidUtilities.java b/common-src/com/todoroo/andlib/utility/AndroidUtilities.java
new file mode 100644
index 000000000..c19380b5c
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/AndroidUtilities.java
@@ -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
+ *
+ */
+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);
+ }
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/utility/Base64.java b/common-src/com/todoroo/andlib/utility/Base64.java
new file mode 100644
index 000000000..60dd008a2
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/Base64.java
@@ -0,0 +1,2052 @@
+package com.todoroo.andlib.utility;
+
+/**
+ * Encodes and decodes to and from Base64 notation.
+ * Homepage: http://iharder.net/base64 .
+ *
+ * Example:
+ *
+ * String encoded = Base64.encode( myByteArray );
+ *
+ * byte[] myByteArray = Base64.decode( encoded );
+ *
+ * The options parameter, which appears in a few places, is used to pass
+ * several pieces of information to the encoder. In the "higher level" methods such as
+ * encodeBytes( bytes, options ) the options parameter can be used to indicate such
+ * things as first gzipping the bytes before encoding them, not inserting linefeeds,
+ * and encoding using the URL-safe and Ordered dialects.
+ *
+ * Note, according to RFC3548 ,
+ * Section 2.1, implementations should not add line feeds unless explicitly told
+ * to do so. I've got Base64 set to this behavior now, although earlier versions
+ * broke lines by default.
+ *
+ * The constants defined in Base64 can be OR-ed together to combine options, so you
+ * might make a call like this:
+ *
+ * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES );
+ * to compress the data before encoding it and then making the output have newline characters.
+ * Also...
+ * String encoded = Base64.encodeBytes( crazyString.getBytes() );
+ *
+ *
+ *
+ *
+ * Change Log:
+ *
+ *
+ * v2.3.4 - Fixed bug when working with gzipped streams whereby flushing
+ * the Base64.OutputStream closed the Base64 encoding (by padding with equals
+ * signs) too soon. Also added an option to suppress the automatic decoding
+ * of gzipped streams. Also added experimental support for specifying a
+ * class loader when using the
+ * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)}
+ * method.
+ * v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java
+ * footprint with its CharEncoders and so forth. Fixed some javadocs that were
+ * inconsistent. Removed imports and specified things like java.io.IOException
+ * explicitly inline.
+ * v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the
+ * final encoded data will be so that the code doesn't have to create two output
+ * arrays: an oversized initial one and then a final, exact-sized one. Big win
+ * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not
+ * using the gzip options which uses a different mechanism with streams and stuff).
+ * v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some
+ * similar helper methods to be more efficient with memory by not returning a
+ * String but just a byte array.
+ * v2.3 - This is not a drop-in replacement! This is two years of comments
+ * and bug fixes queued up and finally executed. Thanks to everyone who sent
+ * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else.
+ * Much bad coding was cleaned up including throwing exceptions where necessary
+ * instead of returning null values or something similar. Here are some changes
+ * that may affect you:
+ *
+ * Does not break lines, by default. This is to keep in compliance with
+ * RFC3548 .
+ * Throws exceptions instead of returning null values. Because some operations
+ * (especially those that may permit the GZIP option) use IO streams, there
+ * is a possiblity of an java.io.IOException being thrown. After some discussion and
+ * thought, I've changed the behavior of the methods to throw java.io.IOExceptions
+ * rather than return null if ever there's an error. I think this is more
+ * appropriate, though it will require some changes to your code. Sorry,
+ * it should have been done this way to begin with.
+ * Removed all references to System.out, System.err, and the like.
+ * Shame on me. All I can say is sorry they were ever there.
+ * Throws NullPointerExceptions and IllegalArgumentExceptions as needed
+ * such as when passed arrays are null or offsets are invalid.
+ * Cleaned up as much javadoc as I could to avoid any javadoc warnings.
+ * This was especially annoying before for people who were thorough in their
+ * own projects and then had gobs of javadoc warnings on this file.
+ *
+ * v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug
+ * when using very small files (~< 40 bytes).
+ * v2.2 - Added some helper methods for encoding/decoding directly from
+ * one file to the next. Also added a main() method to support command line
+ * encoding/decoding from one file to the next. Also added these Base64 dialects:
+ *
+ * The default is RFC3548 format.
+ * Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates
+ * URL and file name friendly format as described in Section 4 of RFC3548.
+ * http://www.faqs.org/rfcs/rfc3548.html
+ * Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates
+ * URL and file name friendly format that preserves lexical ordering as described
+ * in http://www.faqs.org/qa/rfcc-1940.html
+ *
+ * Special thanks to Jim Kellerman at http://www.powerset.com/
+ * for contributing the new Base64 dialects.
+ *
+ *
+ * v2.1 - Cleaned up javadoc comments and unused variables and methods. Added
+ * some convenience methods for reading and writing to and from files.
+ * v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems
+ * with other encodings (like EBCDIC).
+ * v2.0.1 - Fixed an error when decoding a single byte, that is, when the
+ * encoded data was a single byte.
+ * v2.0 - I got rid of methods that used booleans to set options.
+ * Now everything is more consolidated and cleaner. The code now detects
+ * when data that's being decoded is gzip-compressed and will decompress it
+ * automatically. Generally things are cleaner. You'll probably have to
+ * change some method calls that you were making to support the new
+ * options format (int s that you "OR" together).
+ * v1.5.1 - Fixed bug when decompressing and decoding to a
+ * byte[] using decode( String s, boolean gzipCompressed ) .
+ * Added the ability to "suspend" encoding in the Output Stream so
+ * you can turn on and off the encoding if you need to embed base64
+ * data in an otherwise "normal" stream (like an XML file).
+ * v1.5 - Output stream pases on flush() command but doesn't do anything itself.
+ * This helps when using GZIP streams.
+ * Added the ability to GZip-compress objects before encoding them.
+ * v1.4 - Added helper methods to read/write files.
+ * v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
+ * v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream
+ * where last buffer being read, if not completely full, was not returned.
+ * v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
+ * v1.3.3 - Fixed I/O streams which were totally messed up.
+ *
+ *
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit http://iharder.net/base64
+ * periodically to check for updates or to contribute improvements.
+ *
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.3.3
+ */
+@SuppressWarnings({"nls", "null"})
+public class Base64
+{
+
+/* ******** P U B L I C F I E L D S ******** */
+
+
+ /** No options specified. Value is zero. */
+ public final static int NO_OPTIONS = 0;
+
+ /** Specify encoding in first bit. Value is one. */
+ public final static int ENCODE = 1;
+
+
+ /** Specify decoding in first bit. Value is zero. */
+ public final static int DECODE = 0;
+
+
+ /** Specify that data should be gzip-compressed in second bit. Value is two. */
+ public final static int GZIP = 2;
+
+ /** Specify that gzipped data should not be automatically gunzipped. */
+ public final static int DONT_GUNZIP = 4;
+
+
+ /** Do break lines when encoding. Value is 8. */
+ public final static int DO_BREAK_LINES = 8;
+
+ /**
+ * Encode using Base64-like encoding that is URL- and Filename-safe as described
+ * in Section 4 of RFC3548:
+ * http://www.faqs.org/rfcs/rfc3548.html .
+ * It is important to note that data encoded this way is not officially valid Base64,
+ * or at the very least should not be called Base64 without also specifying that is
+ * was encoded using the URL- and Filename-safe dialect.
+ */
+ public final static int URL_SAFE = 16;
+
+
+ /**
+ * Encode using the special "ordered" dialect of Base64 described here:
+ * http://www.faqs.org/qa/rfcc-1940.html .
+ */
+ public final static int ORDERED = 32;
+
+
+/* ******** P R I V A T E F I E L D S ******** */
+
+
+ /** Maximum line length (76) of Base64 output. */
+ private final static int MAX_LINE_LENGTH = 76;
+
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte)'=';
+
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte)'\n';
+
+
+ /** Preferred encoding. */
+ private final static String PREFERRED_ENCODING = "US-ASCII";
+
+
+ private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
+ private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
+
+
+/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
+
+ /** The 64 valid Base64 values. */
+ /* Host platform me be something funny like EBCDIC, so we hardcode these values. */
+ private final static byte[] _STANDARD_ALPHABET = {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
+ };
+
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ **/
+ private final static byte[] _STANDARD_DECODABET = {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9,-9,-9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N'
+ 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z'
+ -9,-9,-9,-9,-9,-9, // Decimal 91 - 96
+ 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm'
+ 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z'
+ -9,-9,-9,-9 // Decimal 123 - 126
+ /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+
+/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548:
+ * http://www.faqs.org/rfcs/rfc3548.html .
+ * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash."
+ */
+ private final static byte[] _URL_SAFE_ALPHABET = {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_'
+ };
+
+ /**
+ * Used in decoding URL- and Filename-safe dialects of Base64.
+ */
+ private final static byte[] _URL_SAFE_DECODABET = {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 62, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // Slash at decimal 47
+ 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N'
+ 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z'
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 63, // Underscore at decimal 95
+ -9, // Decimal 96
+ 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm'
+ 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z'
+ -9,-9,-9,-9 // Decimal 123 - 126
+ /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+
+
+/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * I don't get the point of this technique, but someone requested it,
+ * and it is described here:
+ * http://www.faqs.org/qa/rfcc-1940.html .
+ */
+ private final static byte[] _ORDERED_ALPHABET = {
+ (byte)'-',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4',
+ (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9',
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'_',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z'
+ };
+
+ /**
+ * Used in decoding the "ordered" dialect of Base64.
+ */
+ private final static byte[] _ORDERED_DECODABET = {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 0, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // Slash at decimal 47
+ 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M'
+ 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z'
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 37, // Underscore at decimal 95
+ -9, // Decimal 96
+ 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm'
+ 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z'
+ -9,-9,-9,-9 // Decimal 123 - 126
+ /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+
+/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */
+
+
+ /**
+ * Returns one of the _SOMETHING_ALPHABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URLSAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ final static byte[] getAlphabet( int options ) {
+ if ((options & URL_SAFE) == URL_SAFE) {
+ return _URL_SAFE_ALPHABET;
+ } else if ((options & ORDERED) == ORDERED) {
+ return _ORDERED_ALPHABET;
+ } else {
+ return _STANDARD_ALPHABET;
+ }
+ } // end getAlphabet
+
+
+ /**
+ * Returns one of the _SOMETHING_DECODABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URL_SAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ final static byte[] getDecodabet( int options ) {
+ if( (options & URL_SAFE) == URL_SAFE) {
+ return _URL_SAFE_DECODABET;
+ } else if ((options & ORDERED) == ORDERED) {
+ return _ORDERED_DECODABET;
+ } else {
+ return _STANDARD_DECODABET;
+ }
+ } // end getAlphabet
+
+
+
+ /** Defeats instantiation. */
+ private Base64(){/**/}
+
+
+
+
+/* ******** E N C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Encodes up to the first three bytes of array threeBytes
+ * and returns a four-byte array in Base64 notation.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes .
+ * The array threeBytes needs only be as big as
+ * numSigBytes .
+ * Code can reuse a byte array by passing a four-byte array as b4 .
+ *
+ * @param b4 A reusable byte array to reduce array instantiation
+ * @param threeBytes the array to convert
+ * @param numSigBytes the number of significant bytes in your array
+ * @return four byte array in Base64 notation.
+ * @since 1.5.1
+ */
+ static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) {
+ encode3to4( threeBytes, 0, numSigBytes, b4, 0, options );
+ return b4;
+ } // end encode3to4
+
+
+ /**
+ * Encodes up to three bytes of the array source
+ * and writes the resulting four Base64 bytes to destination .
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset .
+ * This method does not check to make sure your arrays
+ * are large enough to accomodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes .
+ * This is the lowest level of the encoding methods with
+ * all possible parameters.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @return the destination array
+ * @since 1.3
+ */
+ static byte[] encode3to4(
+ byte[] source, int srcOffset, int numSigBytes,
+ byte[] destination, int destOffset, int options ) {
+
+ byte[] ALPHABET = getAlphabet( options );
+
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index ALPHABET
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 )
+ | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 )
+ | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 );
+
+ switch( numSigBytes )
+ {
+ case 3:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
+ destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ];
+ return destination;
+
+ case 2:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
+ destination[ destOffset + 3 ] = EQUALS_SIGN;
+ return destination;
+
+ case 1:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = EQUALS_SIGN;
+ destination[ destOffset + 3 ] = EQUALS_SIGN;
+ return destination;
+
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+
+
+ /**
+ * Performs Base64 encoding on the raw ByteBuffer,
+ * writing it to the encoded ByteBuffer.
+ * This is an experimental feature. Currently it does not
+ * pass along any options (such as {@link #DO_BREAK_LINES}
+ * or {@link #GZIP}.
+ *
+ * @param raw input buffer
+ * @param encoded output buffer
+ * @since 2.3
+ */
+ public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded ){
+ byte[] raw3 = new byte[3];
+ byte[] enc4 = new byte[4];
+
+ while( raw.hasRemaining() ){
+ int rem = Math.min(3,raw.remaining());
+ raw.get(raw3,0,rem);
+ Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS );
+ encoded.put(enc4);
+ } // end input remaining
+ }
+
+
+ /**
+ * Performs Base64 encoding on the raw ByteBuffer,
+ * writing it to the encoded CharBuffer.
+ * This is an experimental feature. Currently it does not
+ * pass along any options (such as {@link #DO_BREAK_LINES}
+ * or {@link #GZIP}.
+ *
+ * @param raw input buffer
+ * @param encoded output buffer
+ * @since 2.3
+ */
+ public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded ){
+ byte[] raw3 = new byte[3];
+ byte[] enc4 = new byte[4];
+
+ while( raw.hasRemaining() ){
+ int rem = Math.min(3,raw.remaining());
+ raw.get(raw3,0,rem);
+ Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS );
+ for( int i = 0; i < 4; i++ ){
+ encoded.put( (char)(enc4[i] & 0xFF) );
+ }
+ } // end input remaining
+ }
+
+
+
+
+ /**
+ * Serializes an object and returns the Base64-encoded
+ * version of that serialized object.
+ *
+ * As of v 2.3, if the object
+ * cannot be serialized or there is another error,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * The object is not GZip-compressed before being encoded.
+ *
+ * @param serializableObject The object to encode
+ * @return The Base64-encoded object
+ * @throws java.io.IOException if there is an error
+ * @throws NullPointerException if serializedObject is null
+ * @since 1.4
+ */
+ public static String encodeObject( java.io.Serializable serializableObject )
+ throws java.io.IOException {
+ return encodeObject( serializableObject, NO_OPTIONS );
+ } // end encodeObject
+
+
+
+ /**
+ * Serializes an object and returns the Base64-encoded
+ * version of that serialized object.
+ *
+ * As of v 2.3, if the object
+ * cannot be serialized or there is another error,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * The object is not GZip-compressed before being encoded.
+ *
+ * Example options:
+ * GZIP: gzip-compresses object before encoding it.
+ * DO_BREAK_LINES: break lines at 76 characters
+ *
+ *
+ * Example: encodeObject( myObj, Base64.GZIP ) or
+ *
+ * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES )
+ *
+ * @param serializableObject The object to encode
+ * @param options Specified options
+ * @return The Base64-encoded object
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws java.io.IOException if there is an error
+ * @since 2.0
+ */
+ public static String encodeObject( java.io.Serializable serializableObject, int options )
+ throws java.io.IOException {
+
+ if( serializableObject == null ){
+ throw new NullPointerException( "Cannot serialize a null object." );
+ } // end if: null
+
+ // Streams
+ java.io.ByteArrayOutputStream baos = null;
+ java.io.OutputStream b64os = null;
+ java.util.zip.GZIPOutputStream gzos = null;
+ java.io.ObjectOutputStream oos = null;
+
+
+ try {
+ // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
+ baos = new java.io.ByteArrayOutputStream();
+ b64os = new Base64.OutputStream( baos, ENCODE | options );
+ if( (options & GZIP) != 0 ){
+ // Gzip
+ gzos = new java.util.zip.GZIPOutputStream(b64os);
+ oos = new java.io.ObjectOutputStream( gzos );
+ } else {
+ // Not gzipped
+ oos = new java.io.ObjectOutputStream( b64os );
+ }
+ oos.writeObject( serializableObject );
+ } // end try
+ catch( java.io.IOException e ) {
+ // Catch it and then throw it immediately so that
+ // the finally{/**/} block is called for cleanup.
+ throw e;
+ } // end catch
+ finally {
+ try{ oos.close(); } catch( Exception e ){/**/}
+ try{ gzos.close(); } catch( Exception e ){/**/}
+ try{ b64os.close(); } catch( Exception e ){/**/}
+ try{ baos.close(); } catch( Exception e ){/**/}
+ } // end finally
+
+ // Return value according to relevant encoding.
+ try {
+ return new String( baos.toByteArray(), PREFERRED_ENCODING );
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue){
+ // Fall back to some Java default
+ return new String( baos.toByteArray() );
+ } // end catch
+
+ } // end encode
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Does not GZip-compress data.
+ *
+ * @param source The data to convert
+ * @return The data in Base64-encoded form
+ * @throws NullPointerException if source array is null
+ * @since 1.4
+ */
+ public static String encodeBytes( byte[] source ) {
+ // Since we're not going to have the GZIP encoding turned on,
+ // we're not going to have an java.io.IOException thrown, so
+ // we should not force the user to have to catch it.
+ String encoded = null;
+ try {
+ encoded = encodeBytes(source, 0, source.length, NO_OPTIONS);
+ } catch (java.io.IOException ex) {
+ assert false : ex.getMessage();
+ } // end catch
+ assert encoded != null;
+ return encoded;
+ } // end encodeBytes
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * Example options:
+ * GZIP: gzip-compresses object before encoding it.
+ * DO_BREAK_LINES: break lines at 76 characters
+ * Note: Technically, this makes your encoding non-compliant.
+ *
+ *
+ * Example: encodeBytes( myData, Base64.GZIP ) or
+ *
+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )
+ *
+ *
+ *
As of v 2.3, if there is an error with the GZIP stream,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ *
+ * @param source The data to convert
+ * @param options Specified options
+ * @return The Base64-encoded data as a String
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws java.io.IOException if there is an error
+ * @throws NullPointerException if source array is null
+ * @since 2.0
+ */
+ public static String encodeBytes( byte[] source, int options ) throws java.io.IOException {
+ return encodeBytes( source, 0, source.length, options );
+ } // end encodeBytes
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Does not GZip-compress data.
+ *
+ * As of v 2.3, if there is an error,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @return The Base64-encoded data as a String
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are invalid
+ * @since 1.4
+ */
+ public static String encodeBytes( byte[] source, int off, int len ) {
+ // Since we're not going to have the GZIP encoding turned on,
+ // we're not going to have an java.io.IOException thrown, so
+ // we should not force the user to have to catch it.
+ String encoded = null;
+ try {
+ encoded = encodeBytes( source, off, len, NO_OPTIONS );
+ } catch (java.io.IOException ex) {
+ assert false : ex.getMessage();
+ } // end catch
+ assert encoded != null;
+ return encoded;
+ } // end encodeBytes
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * Example options:
+ * GZIP: gzip-compresses object before encoding it.
+ * DO_BREAK_LINES: break lines at 76 characters
+ * Note: Technically, this makes your encoding non-compliant.
+ *
+ *
+ * Example: encodeBytes( myData, Base64.GZIP ) or
+ *
+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )
+ *
+ *
+ *
As of v 2.3, if there is an error with the GZIP stream,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned a null value, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param options Specified options
+ * @return The Base64-encoded data as a String
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws java.io.IOException if there is an error
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are invalid
+ * @since 2.0
+ */
+ public static String encodeBytes( byte[] source, int off, int len, int options ) throws java.io.IOException {
+ byte[] encoded = encodeBytesToBytes( source, off, len, options );
+
+ // Return value according to relevant encoding.
+ try {
+ return new String( encoded, PREFERRED_ENCODING );
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue) {
+ return new String( encoded );
+ } // end catch
+
+ } // end encodeBytes
+
+
+
+
+ /**
+ * Similar to {@link #encodeBytes(byte[])} but returns
+ * a byte array instead of instantiating a String. This is more efficient
+ * if you're working with I/O streams and have large data sets to encode.
+ *
+ *
+ * @param source The data to convert
+ * @return The Base64-encoded data as a byte[] (of ASCII characters)
+ * @throws NullPointerException if source array is null
+ * @since 2.3.1
+ */
+ public static byte[] encodeBytesToBytes( byte[] source ) {
+ byte[] encoded = null;
+ try {
+ encoded = encodeBytesToBytes( source, 0, source.length, Base64.NO_OPTIONS );
+ } catch( java.io.IOException ex ) {
+ assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
+ }
+ return encoded;
+ }
+
+
+ /**
+ * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns
+ * a byte array instead of instantiating a String. This is more efficient
+ * if you're working with I/O streams and have large data sets to encode.
+ *
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param options Specified options
+ * @return The Base64-encoded data as a String
+ * @see Base64#GZIP
+ * @see Base64#DO_BREAK_LINES
+ * @throws java.io.IOException if there is an error
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are invalid
+ * @since 2.3.1
+ */
+ public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws java.io.IOException {
+
+ if( source == null ){
+ throw new NullPointerException( "Cannot serialize a null array." );
+ } // end if: null
+
+ if( off < 0 ){
+ throw new IllegalArgumentException( "Cannot have negative offset: " + off );
+ } // end if: off < 0
+
+ if( len < 0 ){
+ throw new IllegalArgumentException( "Cannot have length offset: " + len );
+ } // end if: len < 0
+
+ if( off + len > source.length ){
+ throw new IllegalArgumentException(
+ String.format( "Cannot have offset of %d and length of %d with array of length %d", off,len,source.length));
+ } // end if: off < 0
+
+
+
+ // Compress?
+ if( (options & GZIP) != 0 ) {
+ java.io.ByteArrayOutputStream baos = null;
+ java.util.zip.GZIPOutputStream gzos = null;
+ Base64.OutputStream b64os = null;
+
+ try {
+ // GZip -> Base64 -> ByteArray
+ baos = new java.io.ByteArrayOutputStream();
+ b64os = new Base64.OutputStream( baos, ENCODE | options );
+ gzos = new java.util.zip.GZIPOutputStream( b64os );
+
+ gzos.write( source, off, len );
+ gzos.close();
+ } // end try
+ catch( java.io.IOException e ) {
+ // Catch it and then throw it immediately so that
+ // the finally{/**/} block is called for cleanup.
+ throw e;
+ } // end catch
+ finally {
+ try{ gzos.close(); } catch( Exception e ){/**/}
+ try{ b64os.close(); } catch( Exception e ){/**/}
+ try{ baos.close(); } catch( Exception e ){/**/}
+ } // end finally
+
+ return baos.toByteArray();
+ } // end if: compress
+
+ // Else, don't compress. Better not to use streams at all then.
+ else {
+ boolean breakLines = (options & DO_BREAK_LINES) > 0;
+
+ //int len43 = len * 4 / 3;
+ //byte[] outBuff = new byte[ ( len43 ) // Main 4:3
+ // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding
+ // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines
+ // Try to determine more precisely how big the array needs to be.
+ // If we get it right, we don't have to do an array copy, and
+ // we save a bunch of memory.
+ int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding
+ if( breakLines ){
+ encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters
+ }
+ byte[] outBuff = new byte[ encLen ];
+
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for( ; d < len2; d+=3, e+=4 ) {
+ encode3to4( source, d+off, 3, outBuff, e, options );
+
+ lineLength += 4;
+ if( breakLines && lineLength >= MAX_LINE_LENGTH )
+ {
+ outBuff[e+4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // en dfor: each piece of array
+
+ if( d < len ) {
+ encode3to4( source, d+off, len - d, outBuff, e, options );
+ e += 4;
+ } // end if: some padding needed
+
+
+ // Only resize array if we didn't guess it right.
+ if( e < outBuff.length - 1 ){
+ byte[] finalOut = new byte[e];
+ System.arraycopy(outBuff,0, finalOut,0,e);
+ //System.err.println("Having to resize array from " + outBuff.length + " to " + e );
+ return finalOut;
+ } else {
+ //System.err.println("No need to resize array.");
+ return outBuff;
+ }
+
+ } // end else: don't compress
+
+ } // end encodeBytesToBytes
+
+
+
+
+
+/* ******** D E C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Decodes four bytes from array source
+ * and writes the resulting bytes (up to three of them)
+ * to destination .
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset .
+ * This method does not check to make sure your arrays
+ * are large enough to accomodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ * This is the lowest level of the decoding methods with
+ * all possible parameters.
+ *
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param options alphabet type is pulled from this (standard, url-safe, ordered)
+ * @return the number of decoded bytes converted
+ * @throws NullPointerException if source or destination arrays are null
+ * @throws IllegalArgumentException if srcOffset or destOffset are invalid
+ * or there is not enough room in the array.
+ * @since 1.3
+ */
+ static int decode4to3(
+ byte[] source, int srcOffset,
+ byte[] destination, int destOffset, int options ) {
+
+ // Lots of error checking and exception throwing
+ if( source == null ){
+ throw new NullPointerException( "Source array was null." );
+ } // end if
+ if( destination == null ){
+ throw new NullPointerException( "Destination array was null." );
+ } // end if
+ if( srcOffset < 0 || srcOffset + 3 >= source.length ){
+ throw new IllegalArgumentException( String.format(
+ "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) );
+ } // end if
+ if( destOffset < 0 || destOffset +2 >= destination.length ){
+ throw new IllegalArgumentException( String.format(
+ "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) );
+ } // end if
+
+
+ byte[] DECODABET = getDecodabet( options );
+
+ // Example: Dk==
+ if( source[ srcOffset + 2] == EQUALS_SIGN ) {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
+ // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
+ int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
+ | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 );
+
+ destination[ destOffset ] = (byte)( outBuff >>> 16 );
+ return 1;
+ }
+
+ // Example: DkL=
+ else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
+ // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+ // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
+ int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
+ | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
+ | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 );
+
+ destination[ destOffset ] = (byte)( outBuff >>> 16 );
+ destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 );
+ return 2;
+ }
+
+ // Example: DkLE
+ else {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
+ // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+ // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
+ // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
+ int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
+ | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
+ | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6)
+ | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) );
+
+
+ destination[ destOffset ] = (byte)( outBuff >> 16 );
+ destination[ destOffset + 1 ] = (byte)( outBuff >> 8 );
+ destination[ destOffset + 2 ] = (byte)( outBuff );
+
+ return 3;
+ }
+ } // end decodeToBytes
+
+
+
+
+
+ /**
+ * Low-level access to decoding ASCII characters in
+ * the form of a byte array. Ignores GUNZIP option, if
+ * it's set. This is not generally a recommended method,
+ * although it is used internally as part of the decoding process.
+ * Special case: if len = 0, an empty array is returned. Still,
+ * if you need more speed and reduced memory footprint (and aren't
+ * gzipping), consider this method.
+ *
+ * @param source The Base64 encoded data
+ * @return decoded data
+ * @since 2.3.1
+ */
+ public static byte[] decode( byte[] source ){
+ byte[] decoded = null;
+ try {
+ decoded = decode( source, 0, source.length, Base64.NO_OPTIONS );
+ } catch( java.io.IOException ex ) {
+ assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
+ }
+ return decoded;
+ }
+
+
+
+ /**
+ * Low-level access to decoding ASCII characters in
+ * the form of a byte array. Ignores GUNZIP option, if
+ * it's set. This is not generally a recommended method,
+ * although it is used internally as part of the decoding process.
+ * Special case: if len = 0, an empty array is returned. Still,
+ * if you need more speed and reduced memory footprint (and aren't
+ * gzipping), consider this method.
+ *
+ * @param source The Base64 encoded data
+ * @param off The offset of where to begin decoding
+ * @param len The length of characters to decode
+ * @param options Can specify options such as alphabet type to use
+ * @return decoded data
+ * @throws java.io.IOException If bogus characters exist in source data
+ * @since 1.3
+ */
+ public static byte[] decode( byte[] source, int off, int len, int options )
+ throws java.io.IOException {
+
+ // Lots of error checking and exception throwing
+ if( source == null ){
+ throw new NullPointerException( "Cannot decode null source array." );
+ } // end if
+ if( off < 0 || off + len > source.length ){
+ throw new IllegalArgumentException( String.format(
+ "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) );
+ } // end if
+
+ if( len == 0 ){
+ return new byte[0];
+ }else if( len < 4 ){
+ throw new IllegalArgumentException(
+ "Base64-encoded string must have at least four characters, but length specified was " + len );
+ } // end if
+
+ byte[] DECODABET = getDecodabet( options );
+
+ int len34 = len * 3 / 4; // Estimate on array size
+ byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output
+ int outBuffPosn = 0; // Keep track of where we're writing
+
+ byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space
+ int b4Posn = 0; // Keep track of four byte input buffer
+ int i = 0; // Source array counter
+ byte sbiCrop = 0; // Low seven bits (ASCII) of input
+ byte sbiDecode = 0; // Special value from DECODABET
+
+ for( i = off; i < off+len; i++ ) { // Loop through source
+
+ sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits
+ sbiDecode = DECODABET[ sbiCrop ]; // Special value
+
+ // White space, Equals sign, or legit Base64 character
+ // Note the values such as -5 and -9 in the
+ // DECODABETs at the top of the file.
+ if( sbiDecode >= WHITE_SPACE_ENC ) {
+ if( sbiDecode >= EQUALS_SIGN_ENC ) {
+ b4[ b4Posn++ ] = sbiCrop; // Save non-whitespace
+ if( b4Posn > 3 ) { // Time to decode?
+ outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options );
+ b4Posn = 0;
+
+ // If that was the equals sign, break out of 'for' loop
+ if( sbiCrop == EQUALS_SIGN ) {
+ break;
+ } // end if: equals sign
+ } // end if: quartet built
+ } // end if: equals sign or better
+ } // end if: white space, equals sign or better
+ else {
+ // There's a bad input character in the Base64 stream.
+ throw new java.io.IOException( String.format(
+ "Bad Base64 input character '%c' in array position %d", source[i], i ) );
+ } // end else:
+ } // each input character
+
+ byte[] out = new byte[ outBuffPosn ];
+ System.arraycopy( outBuff, 0, out, 0, outBuffPosn );
+ return out;
+ } // end decode
+
+
+
+
+ /**
+ * Decodes data from Base64 notation, automatically
+ * detecting gzip-compressed data and decompressing it.
+ *
+ * @param s the string to decode
+ * @return the decoded data
+ * @throws java.io.IOException If there is a problem
+ * @since 1.4
+ */
+ public static byte[] decode( String s ) throws java.io.IOException {
+ return decode( s, NO_OPTIONS );
+ }
+
+
+
+ /**
+ * Decodes data from Base64 notation, automatically
+ * detecting gzip-compressed data and decompressing it.
+ *
+ * @param s the string to decode
+ * @param options encode options such as URL_SAFE
+ * @return the decoded data
+ * @throws java.io.IOException if there is an error
+ * @throws NullPointerException if s is null
+ * @since 1.4
+ */
+ public static byte[] decode( String s, int options ) throws java.io.IOException {
+
+ if( s == null ){
+ throw new NullPointerException( "Input string was null." );
+ } // end if
+
+ byte[] bytes;
+ try {
+ bytes = s.getBytes( PREFERRED_ENCODING );
+ } // end try
+ catch( java.io.UnsupportedEncodingException uee ) {
+ bytes = s.getBytes();
+ } // end catch
+ //
+
+ // Decode
+ bytes = decode( bytes, 0, bytes.length, options );
+
+ // Check to see if it's gzip-compressed
+ // GZIP Magic Two-Byte Number: 0x8b1f (35615)
+ boolean dontGunzip = (options & DONT_GUNZIP) != 0;
+ if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) {
+
+ int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
+ if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) {
+ java.io.ByteArrayInputStream bais = null;
+ java.util.zip.GZIPInputStream gzis = null;
+ java.io.ByteArrayOutputStream baos = null;
+ byte[] buffer = new byte[2048];
+ int length = 0;
+
+ try {
+ baos = new java.io.ByteArrayOutputStream();
+ bais = new java.io.ByteArrayInputStream( bytes );
+ gzis = new java.util.zip.GZIPInputStream( bais );
+
+ while( ( length = gzis.read( buffer ) ) >= 0 ) {
+ baos.write(buffer,0,length);
+ } // end while: reading input
+
+ // No error? Get new bytes.
+ bytes = baos.toByteArray();
+
+ } // end try
+ catch( java.io.IOException e ) {
+ e.printStackTrace();
+ // Just return originally-decoded bytes
+ } // end catch
+ finally {
+ try{ baos.close(); } catch( Exception e ){/**/}
+ try{ gzis.close(); } catch( Exception e ){/**/}
+ try{ bais.close(); } catch( Exception e ){/**/}
+ } // end finally
+
+ } // end if: gzipped
+ } // end if: bytes.length >= 2
+
+ return bytes;
+ } // end decode
+
+
+
+ /**
+ * Attempts to decode Base64 data and deserialize a Java
+ * Object within. Returns null if there was an error.
+ *
+ * @param encodedObject The Base64 data to decode
+ * @return The decoded and deserialized object
+ * @throws NullPointerException if encodedObject is null
+ * @throws java.io.IOException if there is a general error
+ * @throws ClassNotFoundException if the decoded object is of a
+ * class that cannot be found by the JVM
+ * @since 1.5
+ */
+ public static Object decodeToObject( String encodedObject )
+ throws java.io.IOException, java.lang.ClassNotFoundException {
+ return decodeToObject(encodedObject,NO_OPTIONS,null);
+ }
+
+
+ /**
+ * Attempts to decode Base64 data and deserialize a Java
+ * Object within. Returns null if there was an error.
+ * If loader is not null, it will be the class loader
+ * used when deserializing.
+ *
+ * @param encodedObject The Base64 data to decode
+ * @param options Various parameters related to decoding
+ * @param loader Optional class loader to use in deserializing classes.
+ * @return The decoded and deserialized object
+ * @throws NullPointerException if encodedObject is null
+ * @throws java.io.IOException if there is a general error
+ * @throws ClassNotFoundException if the decoded object is of a
+ * class that cannot be found by the JVM
+ * @since 2.3.4
+ */
+ public static Object decodeToObject(
+ String encodedObject, int options, final ClassLoader loader )
+ throws java.io.IOException, java.lang.ClassNotFoundException {
+
+ // Decode and gunzip if necessary
+ byte[] objBytes = decode( encodedObject, options );
+
+ java.io.ByteArrayInputStream bais = null;
+ java.io.ObjectInputStream ois = null;
+ Object obj = null;
+
+ try {
+ bais = new java.io.ByteArrayInputStream( objBytes );
+
+ // If no custom class loader is provided, use Java's builtin OIS.
+ if( loader == null ){
+ ois = new java.io.ObjectInputStream( bais );
+ } // end if: no loader provided
+
+ // Else make a customized object input stream that uses
+ // the provided class loader.
+ else {
+ ois = new java.io.ObjectInputStream(bais){
+ @Override
+ public Class> resolveClass(java.io.ObjectStreamClass streamClass)
+ throws java.io.IOException, ClassNotFoundException {
+ Class> c = Class.forName(streamClass.getName(), false, loader);
+ if( c == null ){
+ return super.resolveClass(streamClass);
+ } else {
+ return c; // Class loader knows of this class.
+ } // end else: not null
+ } // end resolveClass
+ }; // end ois
+ } // end else: no custom class loader
+
+ obj = ois.readObject();
+ } // end try
+ catch( java.io.IOException e ) {
+ throw e; // Catch and throw in order to execute finally{/**/}
+ } // end catch
+ catch( java.lang.ClassNotFoundException e ) {
+ throw e; // Catch and throw in order to execute finally{/**/}
+ } // end catch
+ finally {
+ try{ bais.close(); } catch( Exception e ){/**/}
+ try{ ois.close(); } catch( Exception e ){/**/}
+ } // end finally
+
+ return obj;
+ } // end decodeObject
+
+
+
+ /**
+ * Convenience method for encoding data to a file.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param dataToEncode byte array of data to encode in base64 form
+ * @param filename Filename for saving encoded data
+ * @throws java.io.IOException if there is an error
+ * @throws NullPointerException if dataToEncode is null
+ * @since 2.1
+ */
+ public static void encodeToFile( byte[] dataToEncode, String filename )
+ throws java.io.IOException {
+
+ if( dataToEncode == null ){
+ throw new NullPointerException( "Data to encode was null." );
+ } // end iff
+
+ Base64.OutputStream bos = null;
+ try {
+ bos = new Base64.OutputStream(
+ new java.io.FileOutputStream( filename ), Base64.ENCODE );
+ bos.write( dataToEncode );
+ } // end try
+ catch( java.io.IOException e ) {
+ throw e; // Catch and throw to execute finally{/**/} block
+ } // end catch: java.io.IOException
+ finally {
+ try{ bos.close(); } catch( Exception e ){/**/}
+ } // end finally
+
+ } // end encodeToFile
+
+
+ /**
+ * Convenience method for decoding data to a file.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param dataToDecode Base64-encoded data as a string
+ * @param filename Filename for saving decoded data
+ * @throws java.io.IOException if there is an error
+ * @since 2.1
+ */
+ public static void decodeToFile( String dataToDecode, String filename )
+ throws java.io.IOException {
+
+ Base64.OutputStream bos = null;
+ try{
+ bos = new Base64.OutputStream(
+ new java.io.FileOutputStream( filename ), Base64.DECODE );
+ bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) );
+ } // end try
+ catch( java.io.IOException e ) {
+ throw e; // Catch and throw to execute finally{/**/} block
+ } // end catch: java.io.IOException
+ finally {
+ try{ bos.close(); } catch( Exception e ){/**/}
+ } // end finally
+
+ } // end decodeToFile
+
+
+
+
+ /**
+ * Convenience method for reading a base64-encoded
+ * file and decoding it.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param filename Filename for reading encoded data
+ * @return decoded byte array
+ * @throws java.io.IOException if there is an error
+ * @since 2.1
+ */
+ public static byte[] decodeFromFile( String filename )
+ throws java.io.IOException {
+
+ byte[] decodedData = null;
+ Base64.InputStream bis = null;
+ try
+ {
+ // Set up some useful variables
+ java.io.File file = new java.io.File( filename );
+ byte[] buffer = null;
+ int length = 0;
+ int numBytes = 0;
+
+ // Check for size of file
+ if( file.length() > Integer.MAX_VALUE )
+ {
+ throw new java.io.IOException( "File is too big for this convenience method (" + file.length() + " bytes)." );
+ } // end if: file too big for int index
+ buffer = new byte[ (int)file.length() ];
+
+ // Open a stream
+ bis = new Base64.InputStream(
+ new java.io.BufferedInputStream(
+ new java.io.FileInputStream( file ) ), Base64.DECODE );
+
+ // Read until done
+ while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) {
+ length += numBytes;
+ } // end while
+
+ // Save in a variable to return
+ decodedData = new byte[ length ];
+ System.arraycopy( buffer, 0, decodedData, 0, length );
+
+ } // end try
+ catch( java.io.IOException e ) {
+ throw e; // Catch and release to execute finally{/**/}
+ } // end catch: java.io.IOException
+ finally {
+ try{ bis.close(); } catch( Exception e) {/**/}
+ } // end finally
+
+ return decodedData;
+ } // end decodeFromFile
+
+
+
+ /**
+ * Convenience method for reading a binary file
+ * and base64-encoding it.
+ *
+ * As of v 2.3, if there is a error,
+ * the method will throw an java.io.IOException. This is new to v2.3!
+ * In earlier versions, it just returned false, but
+ * in retrospect that's a pretty poor way to handle it.
+ *
+ * @param filename Filename for reading binary data
+ * @return base64-encoded string
+ * @throws java.io.IOException if there is an error
+ * @since 2.1
+ */
+ public static String encodeFromFile( String filename )
+ throws java.io.IOException {
+
+ String encodedData = null;
+ Base64.InputStream bis = null;
+ try
+ {
+ // Set up some useful variables
+ java.io.File file = new java.io.File( filename );
+ byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4),40) ]; // Need max() for math on small files (v2.2.1)
+ int length = 0;
+ int numBytes = 0;
+
+ // Open a stream
+ bis = new Base64.InputStream(
+ new java.io.BufferedInputStream(
+ new java.io.FileInputStream( file ) ), Base64.ENCODE );
+
+ // Read until done
+ while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) {
+ length += numBytes;
+ } // end while
+
+ // Save in a variable to return
+ encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING );
+
+ } // end try
+ catch( java.io.IOException e ) {
+ throw e; // Catch and release to execute finally{/**/}
+ } // end catch: java.io.IOException
+ finally {
+ try{ bis.close(); } catch( Exception e) {/**/}
+ } // end finally
+
+ return encodedData;
+ } // end encodeFromFile
+
+ /**
+ * Reads infile and encodes it to outfile .
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @throws java.io.IOException if there is an error
+ * @since 2.2
+ */
+ public static void encodeFileToFile( String infile, String outfile )
+ throws java.io.IOException {
+
+ String encoded = Base64.encodeFromFile( infile );
+ java.io.OutputStream out = null;
+ try{
+ out = new java.io.BufferedOutputStream(
+ new java.io.FileOutputStream( outfile ) );
+ out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output.
+ } // end try
+ catch( java.io.IOException e ) {
+ throw e; // Catch and release to execute finally{/**/}
+ } // end catch
+ finally {
+ try { out.close(); }
+ catch( Exception ex ){/**/}
+ } // end finally
+ } // end encodeFileToFile
+
+
+ /**
+ * Reads infile and decodes it to outfile .
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @throws java.io.IOException if there is an error
+ * @since 2.2
+ */
+ public static void decodeFileToFile( String infile, String outfile )
+ throws java.io.IOException {
+
+ byte[] decoded = Base64.decodeFromFile( infile );
+ java.io.OutputStream out = null;
+ try{
+ out = new java.io.BufferedOutputStream(
+ new java.io.FileOutputStream( outfile ) );
+ out.write( decoded );
+ } // end try
+ catch( java.io.IOException e ) {
+ throw e; // Catch and release to execute finally{/**/}
+ } // end catch
+ finally {
+ try { out.close(); }
+ catch( Exception ex ){/**/}
+ } // end finally
+ } // end decodeFileToFile
+
+
+ /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */
+
+
+
+ /**
+ * A {@link Base64.InputStream} will read data from another
+ * java.io.InputStream , given in the constructor,
+ * and encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class InputStream extends java.io.FilterInputStream {
+
+ private boolean encode; // Encoding or decoding
+ private int position; // Current position in the buffer
+ private byte[] buffer; // Small buffer holding converted data
+ private int bufferLength; // Length of buffer (3 or 4)
+ private int numSigBytes; // Number of meaningful bytes in the buffer
+ private int lineLength;
+ private boolean breakLines; // Break lines at less than 80 characters
+ private int options; // Record options used to create the stream.
+ private byte[] decodabet; // Local copies to avoid extra method calls
+
+
+ /**
+ * Constructs a {@link Base64.InputStream} in DECODE mode.
+ *
+ * @param in the java.io.InputStream from which to read data.
+ * @since 1.3
+ */
+ public InputStream( java.io.InputStream in ) {
+ this( in, DECODE );
+ } // end constructor
+
+
+ /**
+ * Constructs a {@link Base64.InputStream} in
+ * either ENCODE or DECODE mode.
+ *
+ * Valid options:
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DO_BREAK_LINES: break lines at 76 characters
+ * (only meaningful when encoding)
+ *
+ *
+ * Example: new Base64.InputStream( in, Base64.DECODE )
+ *
+ *
+ * @param in the java.io.InputStream from which to read data.
+ * @param options Specified options
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DO_BREAK_LINES
+ * @since 2.0
+ */
+ public InputStream( java.io.InputStream in, int options ) {
+
+ super( in );
+ this.options = options; // Record for later
+ this.breakLines = (options & DO_BREAK_LINES) > 0;
+ this.encode = (options & ENCODE) > 0;
+ this.bufferLength = encode ? 4 : 3;
+ this.buffer = new byte[ bufferLength ];
+ this.position = -1;
+ this.lineLength = 0;
+ this.decodabet = getDecodabet(options);
+ } // end constructor
+
+ /**
+ * Reads enough of the input stream to convert
+ * to/from Base64 and returns the next byte.
+ *
+ * @return next byte
+ * @since 1.3
+ */
+ @Override
+ public int read() throws java.io.IOException {
+
+ // Do we need to get data?
+ if( position < 0 ) {
+ if( encode ) {
+ byte[] b3 = new byte[3];
+ int numBinaryBytes = 0;
+ for( int i = 0; i < 3; i++ ) {
+ int b = in.read();
+
+ // If end of stream, b is -1.
+ if( b >= 0 ) {
+ b3[i] = (byte)b;
+ numBinaryBytes++;
+ } else {
+ break; // out of for loop
+ } // end else: end of stream
+
+ } // end for: each needed input byte
+
+ if( numBinaryBytes > 0 ) {
+ encode3to4( b3, 0, numBinaryBytes, buffer, 0, options );
+ position = 0;
+ numSigBytes = 4;
+ } // end if: got data
+ else {
+ return -1; // Must be end of stream
+ } // end else
+ } // end if: encoding
+
+ // Else decoding
+ else {
+ byte[] b4 = new byte[4];
+ int i = 0;
+ for( i = 0; i < 4; i++ ) {
+ // Read four "meaningful" bytes:
+ int b = 0;
+ do{ b = in.read(); }
+ while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC );
+
+ if( b < 0 ) {
+ break; // Reads a -1 if end of stream
+ } // end if: end of stream
+
+ b4[i] = (byte)b;
+ } // end for: each needed input byte
+
+ if( i == 4 ) {
+ numSigBytes = decode4to3( b4, 0, buffer, 0, options );
+ position = 0;
+ } // end if: got four characters
+ else if( i == 0 ){
+ return -1;
+ } // end else if: also padded correctly
+ else {
+ // Must have broken out from above.
+ throw new java.io.IOException( "Improperly padded Base64 input." );
+ } // end
+
+ } // end else: decode
+ } // end else: get data
+
+ // Got data?
+ if( position >= 0 ) {
+ // End of relevant data?
+ if( /*!encode &&*/ position >= numSigBytes ){
+ return -1;
+ } // end if: got data
+
+ if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) {
+ lineLength = 0;
+ return '\n';
+ } // end if
+ else {
+ lineLength++; // This isn't important when decoding
+ // but throwing an extra "if" seems
+ // just as wasteful.
+
+ int b = buffer[ position++ ];
+
+ if( position >= bufferLength ) {
+ position = -1;
+ } // end if: end
+
+ return b & 0xFF; // This is how you "cast" a byte that's
+ // intended to be unsigned.
+ } // end else
+ } // end if: position >= 0
+
+ // Else error
+ else {
+ throw new java.io.IOException( "Error in Base64 code reading stream." );
+ } // end else
+ } // end read
+
+
+ /**
+ * Calls {@link #read()} repeatedly until the end of stream
+ * is reached or len bytes are read.
+ * Returns number of bytes read into array or -1 if
+ * end of stream is encountered.
+ *
+ * @param dest array to hold values
+ * @param off offset for array
+ * @param len max number of bytes to read into array
+ * @return bytes read into array or -1 if end of stream is encountered.
+ * @since 1.3
+ */
+ @Override
+ public int read( byte[] dest, int off, int len )
+ throws java.io.IOException {
+ int i;
+ int b;
+ for( i = 0; i < len; i++ ) {
+ b = read();
+
+ if( b >= 0 ) {
+ dest[off + i] = (byte) b;
+ }
+ else if( i == 0 ) {
+ return -1;
+ }
+ else {
+ break; // Out of 'for' loop
+ } // Out of 'for' loop
+ } // end for: each byte read
+ return i;
+ } // end read
+
+ } // end inner class InputStream
+
+
+
+
+
+
+ /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */
+
+
+
+ /**
+ * A {@link Base64.OutputStream} will write data to another
+ * java.io.OutputStream , given in the constructor,
+ * and encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class OutputStream extends java.io.FilterOutputStream {
+
+ private boolean encode;
+ private int position;
+ private byte[] buffer;
+ private int bufferLength;
+ private int lineLength;
+ private boolean breakLines;
+ private byte[] b4; // Scratch used in a few places
+ private boolean suspendEncoding;
+ private int options; // Record for later
+ private byte[] decodabet; // Local copies to avoid extra method calls
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in ENCODE mode.
+ *
+ * @param out the java.io.OutputStream to which data will be written.
+ * @since 1.3
+ */
+ public OutputStream( java.io.OutputStream out ) {
+ this( out, ENCODE );
+ } // end constructor
+
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in
+ * either ENCODE or DECODE mode.
+ *
+ * Valid options:
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DO_BREAK_LINES: don't break lines at 76 characters
+ * (only meaningful when encoding)
+ *
+ *
+ * Example: new Base64.OutputStream( out, Base64.ENCODE )
+ *
+ * @param out the java.io.OutputStream to which data will be written.
+ * @param options Specified options.
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DO_BREAK_LINES
+ * @since 1.3
+ */
+ public OutputStream( java.io.OutputStream out, int options ) {
+ super( out );
+ this.breakLines = (options & DO_BREAK_LINES) != 0;
+ this.encode = (options & ENCODE) != 0;
+ this.bufferLength = encode ? 3 : 4;
+ this.buffer = new byte[ bufferLength ];
+ this.position = 0;
+ this.lineLength = 0;
+ this.suspendEncoding = false;
+ this.b4 = new byte[4];
+ this.options = options;
+ this.decodabet = getDecodabet(options);
+ } // end constructor
+
+
+ /**
+ * Writes the byte to the output stream after
+ * converting to/from Base64 notation.
+ * When encoding, bytes are buffered three
+ * at a time before the output stream actually
+ * gets a write() call.
+ * When decoding, bytes are buffered four
+ * at a time.
+ *
+ * @param theByte the byte to write
+ * @since 1.3
+ */
+ @Override
+ public void write(int theByte)
+ throws java.io.IOException {
+ // Encoding suspended?
+ if( suspendEncoding ) {
+ this.out.write( theByte );
+ return;
+ } // end if: supsended
+
+ // Encode?
+ if( encode ) {
+ buffer[ position++ ] = (byte)theByte;
+ if( position >= bufferLength ) { // Enough to encode.
+
+ this.out.write( encode3to4( b4, buffer, bufferLength, options ) );
+
+ lineLength += 4;
+ if( breakLines && lineLength >= MAX_LINE_LENGTH ) {
+ this.out.write( NEW_LINE );
+ lineLength = 0;
+ } // end if: end of line
+
+ position = 0;
+ } // end if: enough to output
+ } // end if: encoding
+
+ // Else, Decoding
+ else {
+ // Meaningful Base64 character?
+ if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) {
+ buffer[ position++ ] = (byte)theByte;
+ if( position >= bufferLength ) { // Enough to output.
+
+ int len = Base64.decode4to3( buffer, 0, b4, 0, options );
+ out.write( b4, 0, len );
+ position = 0;
+ } // end if: enough to output
+ } // end if: meaningful base64 character
+ else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) {
+ throw new java.io.IOException( "Invalid character in Base64 data." );
+ } // end else: not white space either
+ } // end else: decoding
+ } // end write
+
+
+
+ /**
+ * Calls {@link #write(int)} repeatedly until len
+ * bytes are written.
+ *
+ * @param theBytes array from which to read bytes
+ * @param off offset for array
+ * @param len max number of bytes to read into array
+ * @since 1.3
+ */
+ @Override
+ public void write( byte[] theBytes, int off, int len )
+ throws java.io.IOException {
+ // Encoding suspended?
+ if( suspendEncoding ) {
+ this.out.write( theBytes, off, len );
+ return;
+ } // end if: supsended
+
+ for( int i = 0; i < len; i++ ) {
+ write( theBytes[ off + i ] );
+ } // end for: each byte written
+
+ } // end write
+
+
+
+ /**
+ * Method added by PHIL. [Thanks, PHIL. -Rob]
+ * This pads the buffer without closing the stream.
+ * @throws java.io.IOException if there's an error.
+ */
+ public void flushBase64() throws java.io.IOException {
+ if( position > 0 ) {
+ if( encode ) {
+ out.write( encode3to4( b4, buffer, position, options ) );
+ position = 0;
+ } // end if: encoding
+ else {
+ throw new java.io.IOException( "Base64 input not properly padded." );
+ } // end else: decoding
+ } // end if: buffer partially full
+
+ } // end flush
+
+
+ /**
+ * Flushes and closes (I think, in the superclass) the stream.
+ *
+ * @since 1.3
+ */
+ @Override
+ public void close() throws java.io.IOException {
+ // 1. Ensure that pending characters are written
+ flushBase64();
+
+ // 2. Actually close the stream
+ // Base class both flushes and closes.
+ super.close();
+
+ buffer = null;
+ out = null;
+ } // end close
+
+
+
+ /**
+ * Suspends encoding of the stream.
+ * May be helpful if you need to embed a piece of
+ * base64-encoded data in a stream.
+ *
+ * @throws java.io.IOException if there's an error flushing
+ * @since 1.5.1
+ */
+ public void suspendEncoding() throws java.io.IOException {
+ flushBase64();
+ this.suspendEncoding = true;
+ } // end suspendEncoding
+
+
+ /**
+ * Resumes encoding of the stream.
+ * May be helpful if you need to embed a piece of
+ * base64-encoded data in a stream.
+ *
+ * @since 1.5.1
+ */
+ public void resumeEncoding() {
+ this.suspendEncoding = false;
+ } // end resumeEncoding
+
+
+
+ } // end inner class OutputStream
+
+
+} // end class Base64
diff --git a/common-src/com/todoroo/andlib/utility/DateUtilities.java b/common-src/com/todoroo/andlib/utility/DateUtilities.java
new file mode 100644
index 000000000..39685bf13
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/DateUtilities.java
@@ -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();
+ }
+
+}
diff --git a/common-src/com/todoroo/andlib/utility/DialogUtilities.java b/common-src/com/todoroo/andlib/utility/DialogUtilities.java
new file mode 100644
index 000000000..895cb37ea
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/DialogUtilities.java
@@ -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
+ }
+ }
+ });
+ }
+}
diff --git a/common-src/com/todoroo/andlib/utility/EmailValidator.java b/common-src/com/todoroo/andlib/utility/EmailValidator.java
new file mode 100644
index 000000000..2029596d6
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/EmailValidator.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/common-src/com/todoroo/andlib/utility/Pair.java b/common-src/com/todoroo/andlib/utility/Pair.java
new file mode 100644
index 000000000..d3001a568
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/Pair.java
@@ -0,0 +1,56 @@
+package com.todoroo.andlib.utility;
+
+/**
+ * Pair utility class
+ *
+ * @author Tim Su
+ *
+ * @param
+ * @param
+ */
+public class Pair {
+
+ 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 Pair create(A left, B right) {
+ return new Pair (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);
+ }
+}
diff --git a/common-src/com/todoroo/andlib/utility/SoftHashMap.java b/common-src/com/todoroo/andlib/utility/SoftHashMap.java
new file mode 100644
index 000000000..188b31980
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/SoftHashMap.java
@@ -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
+ *
+ *
+ * @param
+ * @param
+ */
+public class SoftHashMap extends AbstractMap implements
+ Serializable {
+ private static final long serialVersionUID = -3796460667941300642L;
+
+ /** The internal HashMap that will hold the SoftReference. */
+ private final Map> hash = new HashMap>();
+
+ private final Map, K> reverseLookup = new HashMap, K>();
+
+ /** Reference queue for cleared SoftReference objects. */
+ protected final ReferenceQueue queue = new ReferenceQueue();
+
+ @Override
+ public V get(Object key) {
+ expungeStaleEntries();
+ V result = null;
+ // We get the SoftReference represented by that key
+ SoftReference 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 soft_ref = new SoftReference(value, queue);
+ reverseLookup.put(soft_ref, key);
+ SoftReference 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 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> entrySet() {
+ expungeStaleEntries();
+ Set> result = new LinkedHashSet>();
+ for (final Entry> entry : hash.entrySet()) {
+ final V value = entry.getValue().get();
+ if (value != null) {
+ result.add(new Entry() {
+ public K getKey() {
+ return entry.getKey();
+ }
+
+ public V getValue() {
+ return value;
+ }
+
+ public V setValue(V v) {
+ entry.setValue(new SoftReference(v, queue));
+ return value;
+ }
+ });
+ }
+ }
+ return result;
+ }
+}
diff --git a/common-src/com/todoroo/andlib/utility/UserTask.java b/common-src/com/todoroo/andlib/utility/UserTask.java
new file mode 100644
index 000000000..9440a49ae
--- /dev/null
+++ b/common-src/com/todoroo/andlib/utility/UserTask.java
@@ -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;
+
+/**
+ * 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.
+ *
+ * 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 Params, Progress and Result,
+ * and 4 steps, called begin, doInBackground,
+ * processProgress and end.
+ *
+ * Usage
+ * 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)}.)
+ *
+ * Here is an example of subclassing:
+ *
+ * private class DownloadFilesTask extends UserTask<URL, Integer, Long> {
+ * public File doInBackground(URL... urls) {
+ * int count = urls.length;
+ * long totalSize = 0;
+ * for (int i = 0; i < count; i++) {
+ * totalSize += Downloader.downloadFile(urls[i]);
+ * publishProgress((int) ((i / (float) count) * 100));
+ * }
+ * }
+ *
+ * public void processProgress(Integer... progress) {
+ * setProgressPercent(progress[0]);
+ * }
+ *
+ * public void end(Long result) {
+ * showDialog("Downloaded " + result + " bytes");
+ * }
+ * }
+ *
+ *
+ * Once created, a task is executed very simply:
+ *
+ * new DownloadFilesTask().execute(new URL[] { ... });
+ *
+ *
+ * User task's generic types
+ * The three types used by a user task are the following:
+ *
+ * Params, the type of the parameters sent to the task upon
+ * execution.
+ * Progress, the type of the progress units published during
+ * the background computation.
+ * Result, the type of the result of the background
+ * computation.
+ *
+ * Not all types are always used by a user task. To mark a type as unused,
+ * simply use the type {@link Void}:
+ *
+ * private class MyTask extends UserTask
+ *
+ * The 4 steps
+ * When a user task is executed, the task goes through 4 steps:
+ *
+ * {@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.
+ * {@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.
+ * {@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.
+ * {@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.
+ *
+ *
+ * Threading rules
+ * There are a few threading rules that must be followed for this class to
+ * work properly:
+ *
+ * The task instance must be created on the UI thread.
+ * {@link #execute(Object[])} must be invoked on the UI thread.
+ * Do not call {@link #begin()}, {@link #end(Object)},
+ * {@link #doInBackground(Object[])}, {@link #processProgress(Object[])}
+ * manually.
+ * The task can be executed only once (an exception will be thrown if
+ * a second execution is attempted.)
+ *
+ */
+@SuppressWarnings("nls")
+public abstract class UserTask {
+ 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 sWorkQueue =
+ new LinkedBlockingQueue(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 mWorker;
+ private final FutureTask 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() {
+ public Result call() throws Exception {
+ return doInBackground(mParams);
+ }
+ };
+
+ mFuture = new FutureTask(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(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 true if this task was cancelled before it completed
+ * normally.
+ *
+ * @return true 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 cancel is called,
+ * this task should never run. If the task has already started,
+ * then the mayInterruptIfRunning parameter determines
+ * whether the thread executing this task should be interrupted in
+ * an attempt to stop the task.
+ *
+ * @param mayInterruptIfRunning true if the thread executing this
+ * task should be interrupted; otherwise, in-progress tasks are allowed
+ * to complete.
+ *
+ * @return false if the task could not be cancelled,
+ * typically because it has already completed normally;
+ * true 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 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(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 implements Callable {
+ Params[] mParams;
+ }
+
+ protected static class UserTaskResult {
+ final UserTask, ?, ?> mTask;
+ final Data[] mData;
+
+ UserTaskResult(UserTask, ?, ?> task, Data... data) {
+ mTask = task;
+ mData = data;
+ }
+ }
+}
diff --git a/common-src/com/todoroo/andlib/widget/DateControlSet.java b/common-src/com/todoroo/andlib/widget/DateControlSet.java
new file mode 100644
index 000000000..e0b715a50
--- /dev/null
+++ b/common-src/com/todoroo/andlib/widget/DateControlSet.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/common-src/com/todoroo/andlib/widget/DateWithNullControlSet.java b/common-src/com/todoroo/andlib/widget/DateWithNullControlSet.java
new file mode 100644
index 000000000..3cd1a1d34
--- /dev/null
+++ b/common-src/com/todoroo/andlib/widget/DateWithNullControlSet.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/common-src/com/todoroo/andlib/widget/NNumberPickerDialog.java b/common-src/com/todoroo/andlib/widget/NNumberPickerDialog.java
new file mode 100644
index 000000000..fa2f9e5c6
--- /dev/null
+++ b/common-src/com/todoroo/andlib/widget/NNumberPickerDialog.java
@@ -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 pickers = new LinkedList();
+ 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);
+ }
+ }
+}
diff --git a/common-src/com/todoroo/andlib/widget/NumberPickerDialog.java b/common-src/com/todoroo/andlib/widget/NumberPickerDialog.java
new file mode 100644
index 000000000..0dc1ea803
--- /dev/null
+++ b/common-src/com/todoroo/andlib/widget/NumberPickerDialog.java
@@ -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());
+ }
+ }
+}
diff --git a/common-src/com/todoroo/andlib/widget/NumberPickerWidget.java b/common-src/com/todoroo/andlib/widget/NumberPickerWidget.java
new file mode 100644
index 000000000..25039a0c9
--- /dev/null
+++ b/common-src/com/todoroo/andlib/widget/NumberPickerWidget.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/common-src/com/todoroo/andlib/widget/NumberPickerWidgetButton.java b/common-src/com/todoroo/andlib/widget/NumberPickerWidgetButton.java
new file mode 100644
index 000000000..dea3c9e47
--- /dev/null
+++ b/common-src/com/todoroo/andlib/widget/NumberPickerWidgetButton.java
@@ -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();
+ }
+ }
+}
\ No newline at end of file