mirror of https://github.com/tasks/tasks
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
360 lines
12 KiB
Java
360 lines
12 KiB
Java
/**
|
|
* Copyright (c) 2012 Todoroo Inc
|
|
*
|
|
* See the file "LICENSE" for the full license governing this code.
|
|
*/
|
|
package com.todoroo.astrid.dao;
|
|
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteConstraintException;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteException;
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
import android.text.TextUtils;
|
|
|
|
import com.todoroo.andlib.data.AbstractModel;
|
|
import com.todoroo.andlib.data.Property;
|
|
import com.todoroo.andlib.data.SqlConstructorVisitor;
|
|
import com.todoroo.andlib.data.Table;
|
|
import com.todoroo.andlib.utility.AndroidUtilities;
|
|
import com.todoroo.astrid.data.Metadata;
|
|
import com.todoroo.astrid.data.StoreObject;
|
|
import com.todoroo.astrid.data.TagData;
|
|
import com.todoroo.astrid.data.Task;
|
|
import com.todoroo.astrid.data.TaskAttachment;
|
|
import com.todoroo.astrid.data.TaskListMetadata;
|
|
import com.todoroo.astrid.data.UserActivity;
|
|
import com.todoroo.astrid.provider.Astrid2TaskProvider;
|
|
import com.todoroo.astrid.provider.Astrid3ContentProvider;
|
|
|
|
import org.tasks.analytics.Tracker;
|
|
import org.tasks.injection.ApplicationScope;
|
|
import org.tasks.injection.ForApplication;
|
|
|
|
import javax.inject.Inject;
|
|
|
|
import timber.log.Timber;
|
|
|
|
/**
|
|
* Database wrapper
|
|
*
|
|
* @author Tim Su <tim@todoroo.com>
|
|
*
|
|
*/
|
|
@ApplicationScope
|
|
public class Database {
|
|
|
|
private static final int VERSION = 38;
|
|
private static final String NAME = "database";
|
|
private static final Table[] TABLES = new Table[] {
|
|
Task.TABLE,
|
|
Metadata.TABLE,
|
|
StoreObject.TABLE,
|
|
TagData.TABLE,
|
|
UserActivity.TABLE,
|
|
TaskAttachment.TABLE,
|
|
TaskListMetadata.TABLE,
|
|
};
|
|
|
|
private final SQLiteOpenHelper helper;
|
|
private final Context context;
|
|
private final Tracker tracker;
|
|
private SQLiteDatabase database;
|
|
|
|
// --- listeners
|
|
|
|
@Inject
|
|
public Database(@ForApplication Context context, Tracker tracker) {
|
|
this.context = context;
|
|
this.tracker = tracker;
|
|
helper = new DatabaseHelper(context, getName(), VERSION);
|
|
}
|
|
|
|
// --- implementation
|
|
|
|
public String getName() {
|
|
return NAME;
|
|
}
|
|
|
|
/**
|
|
* Create indices
|
|
*/
|
|
private void onCreateTables() {
|
|
StringBuilder sql = new StringBuilder();
|
|
sql.append("CREATE INDEX IF NOT EXISTS md_tid ON ").
|
|
append(Metadata.TABLE).append('(').
|
|
append(Metadata.TASK.name).
|
|
append(')');
|
|
database.execSQL(sql.toString());
|
|
sql.setLength(0);
|
|
|
|
sql.append("CREATE INDEX IF NOT EXISTS md_tkid ON ").
|
|
append(Metadata.TABLE).append('(').
|
|
append(Metadata.TASK.name).append(',').
|
|
append(Metadata.KEY.name).
|
|
append(')');
|
|
database.execSQL(sql.toString());
|
|
sql.setLength(0);
|
|
|
|
sql.append("CREATE INDEX IF NOT EXISTS so_id ON ").
|
|
append(StoreObject.TABLE).append('(').
|
|
append(StoreObject.TYPE.name).append(',').
|
|
append(StoreObject.ITEM.name).
|
|
append(')');
|
|
database.execSQL(sql.toString());
|
|
sql.setLength(0);
|
|
|
|
sql.append("CREATE UNIQUE INDEX IF NOT EXISTS t_rid ON ").
|
|
append(Task.TABLE).append('(').
|
|
append(Task.UUID.name).
|
|
append(')');
|
|
database.execSQL(sql.toString());
|
|
sql.setLength(0);
|
|
}
|
|
|
|
private boolean onUpgrade(int oldVersion, int newVersion) {
|
|
SqlConstructorVisitor visitor = new SqlConstructorVisitor();
|
|
switch(oldVersion) {
|
|
case 35:
|
|
tryExecSQL(addColumnSql(TagData.TABLE, TagData.COLOR, visitor, "-1"));
|
|
case 36:
|
|
tryExecSQL(addColumnSql(StoreObject.TABLE, StoreObject.DELETION_DATE, visitor, "0"));
|
|
case 37:
|
|
tryExecSQL(addColumnSql(StoreObject.TABLE, StoreObject.VALUE4, visitor, "-1"));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void tryExecSQL(String sql) {
|
|
try {
|
|
database.execSQL(sql);
|
|
} catch (SQLiteException e) {
|
|
tracker.reportException(e);
|
|
}
|
|
}
|
|
|
|
private static String addColumnSql(Table table, Property<?> property, SqlConstructorVisitor visitor, String defaultValue) {
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.append("ALTER TABLE ")
|
|
.append(table.name)
|
|
.append(" ADD ")
|
|
.append(property.accept(visitor, null));
|
|
if (!TextUtils.isEmpty(defaultValue)) {
|
|
builder.append(" DEFAULT ").append(defaultValue);
|
|
}
|
|
return builder.toString();
|
|
}
|
|
|
|
private void tryAddColumn(Table table, Property<?> column, String defaultValue) {
|
|
try {
|
|
SqlConstructorVisitor visitor = new SqlConstructorVisitor();
|
|
String sql = "ALTER TABLE " + table.name + " ADD " + //$NON-NLS-1$//$NON-NLS-2$
|
|
column.accept(visitor, null);
|
|
if (!TextUtils.isEmpty(defaultValue)) {
|
|
sql += " DEFAULT " + defaultValue;
|
|
}
|
|
database.execSQL(sql);
|
|
} catch (SQLiteException e) {
|
|
// ignored, column already exists
|
|
Timber.e(e, e.getMessage());
|
|
}
|
|
}
|
|
|
|
private String createTableSql(SqlConstructorVisitor visitor,
|
|
String tableName, Property<?>[] properties) {
|
|
StringBuilder sql = new StringBuilder();
|
|
sql.append("CREATE TABLE IF NOT EXISTS ").append(tableName).append('(').
|
|
append(AbstractModel.ID_PROPERTY).append(" INTEGER PRIMARY KEY AUTOINCREMENT");
|
|
for(Property<?> property : properties) {
|
|
if(AbstractModel.ID_PROPERTY.name.equals(property.name)) {
|
|
continue;
|
|
}
|
|
sql.append(',').append(property.accept(visitor, null));
|
|
}
|
|
sql.append(')');
|
|
return sql.toString();
|
|
}
|
|
|
|
private void onDatabaseUpdated() {
|
|
Astrid2TaskProvider.notifyDatabaseModification(context);
|
|
Astrid3ContentProvider.notifyDatabaseModification(context);
|
|
}
|
|
|
|
/**
|
|
* Return the name of the table containing these models
|
|
*/
|
|
public final Table getTable(Class<? extends AbstractModel> modelType) {
|
|
for(Table table : TABLES) {
|
|
if(table.modelClass.equals(modelType)) {
|
|
return table;
|
|
}
|
|
}
|
|
throw new UnsupportedOperationException("Unknown model class " + modelType); //$NON-NLS-1$
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
if(database != null && !database.isReadOnly() && database.isOpen()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
database = helper.getWritableDatabase();
|
|
} catch (NullPointerException e) {
|
|
Timber.e(e, e.getMessage());
|
|
throw new IllegalStateException(e);
|
|
} catch (final RuntimeException original) {
|
|
Timber.e(original, original.getMessage());
|
|
try {
|
|
// provide read-only database
|
|
openForReading();
|
|
} catch (Exception readException) {
|
|
Timber.e(readException, readException.getMessage());
|
|
// throw original write exception
|
|
throw original;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open the database for reading. Must be closed afterwards
|
|
*/
|
|
public synchronized final void openForReading() {
|
|
if(database != null && database.isOpen()) {
|
|
return;
|
|
}
|
|
database = helper.getReadableDatabase();
|
|
}
|
|
|
|
/**
|
|
* Close the database if it has been opened previously
|
|
*/
|
|
public synchronized final void close() {
|
|
if(database != null) {
|
|
database.close();
|
|
}
|
|
database = null;
|
|
}
|
|
|
|
/**
|
|
* @return sql database. opens database if not yet open
|
|
*/
|
|
public synchronized final SQLiteDatabase getDatabase() {
|
|
if(database == null) {
|
|
AndroidUtilities.sleepDeep(300L);
|
|
openForWriting();
|
|
}
|
|
return database;
|
|
}
|
|
|
|
/**
|
|
* @return human-readable database name for debugging
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
return "DB:" + getName();
|
|
}
|
|
|
|
// --- database wrapper
|
|
|
|
public Cursor rawQuery(String sql) {
|
|
return getDatabase().rawQuery(sql, null);
|
|
}
|
|
|
|
public long insert(String table, String nullColumnHack, ContentValues values) {
|
|
long result;
|
|
try {
|
|
result = getDatabase().insertOrThrow(table, nullColumnHack, values);
|
|
} catch (SQLiteConstraintException e) { // Throw these exceptions
|
|
throw e;
|
|
} catch (Exception e) { // Suppress others
|
|
Timber.e(e, e.getMessage());
|
|
result = -1;
|
|
}
|
|
onDatabaseUpdated();
|
|
return result;
|
|
}
|
|
|
|
public int delete(String table, String whereClause, String[] whereArgs) {
|
|
int result = getDatabase().delete(table, whereClause, whereArgs);
|
|
onDatabaseUpdated();
|
|
return result;
|
|
}
|
|
|
|
public int update(String table, ContentValues values, String whereClause) {
|
|
int result = getDatabase().update(table, values, whereClause, null);
|
|
onDatabaseUpdated();
|
|
return result;
|
|
}
|
|
|
|
// --- helper classes
|
|
|
|
/**
|
|
* Default implementation of Astrid database helper
|
|
*/
|
|
private class DatabaseHelper extends SQLiteOpenHelper {
|
|
|
|
public DatabaseHelper(Context context, String name, int version) {
|
|
super(context, name, null, version);
|
|
}
|
|
|
|
/**
|
|
* Called to create the database tables
|
|
*/
|
|
@Override
|
|
public void onCreate(SQLiteDatabase db) {
|
|
StringBuilder sql = new StringBuilder();
|
|
SqlConstructorVisitor sqlVisitor = new SqlConstructorVisitor();
|
|
|
|
// create tables
|
|
for(Table table : TABLES) {
|
|
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 void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
Timber.i("Upgrading database from version %s to %s", oldVersion, newVersion);
|
|
|
|
database = db;
|
|
try {
|
|
if(!Database.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);
|
|
}
|
|
} catch (Exception e) {
|
|
Timber.e(e, "database-upgrade-%s-%s-%s", getName(), oldVersion, newVersion);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|