Remove AbstractDatabase, let android lock sqlite

pull/253/head
Alex Baker 10 years ago
parent 398bea94a1
commit bea0216e66

@ -6,7 +6,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.tasks"
android:versionName="4.7.5"
android:versionCode="342">
android:versionCode="344">
<!-- widgets, alarms, and services will break if Astrid is installed on SD card -->
<!-- android:installLocation="internalOnly"> -->

@ -1,323 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.andlib.data;
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.SQLiteOpenHelper;
import com.todoroo.andlib.data.Property.PropertyVisitor;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.utility.AndroidUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
/**
* AbstractDatabase is a database abstraction which wraps a SQLite database.
* <p>
* Users of this class are in charge of the database's lifecycle - ensuring that
* the database is open when needed and closed when usage is finished. Within an
* activity, this is typically accomplished through the onResume and onPause
* methods, though if the database is not needed for the activity's entire
* lifecycle, it can be closed earlier.
* <p>
* Direct querying is not recommended for type safety reasons. Instead, use one
* of the service classes to issue the request and return a {@link TodorooCursor}.
*
* @author Tim Su <tim@todoroo.com>
*
*/
abstract public class AbstractDatabase {
private static final Logger log = LoggerFactory.getLogger(AbstractDatabase.class);
// --- 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
* @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
*/
private 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;
// --- listeners
/**
* Interface for responding to database changes
*/
public interface DatabaseUpdateListener {
/**
* Called when an INSERT, UPDATE, or DELETE occurs
*/
public void onDatabaseUpdated();
}
private final ArrayList<DatabaseUpdateListener> listeners = new ArrayList<>();
public void addListener(DatabaseUpdateListener listener) {
listeners.add(listener);
}
private void onDatabaseUpdated() {
for(DatabaseUpdateListener listener : listeners) {
listener.onDatabaseUpdated();
}
}
/**
* Return the name of the table containing these models
*/
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$
}
private synchronized void initializeHelper() {
if(helper == null) {
if(ContextManager.getContext() == null) {
throw new NullPointerException("Null context creating database helper");
}
helper = new DatabaseHelper(ContextManager.getContext(), getName(), 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();
if(database != null && !database.isReadOnly() && database.isOpen()) {
return;
}
try {
database = helper.getWritableDatabase();
} catch (NullPointerException e) {
log.error(e.getMessage(), e);
throw new IllegalStateException(e);
} catch (final RuntimeException original) {
log.error(original.getMessage(), original);
try {
// provide read-only database
openForReading();
} catch (Exception readException) {
log.error(readException.getMessage(), readException);
// throw original write exception
throw original;
}
}
}
/**
* Open the database for reading. Must be closed afterwards
*/
public synchronized final void openForReading() {
initializeHelper();
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 synchronized Cursor rawQuery(String sql) {
return getDatabase().rawQuery(sql, null);
}
/*
* @see android.database.sqlite.SQLiteDatabase#insert(String table, String nullColumnHack, ContentValues values)
*/
public synchronized 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
log.error(e.getMessage(), e);
result = -1;
}
onDatabaseUpdated();
return result;
}
/*
* @see android.database.sqlite.SQLiteDatabase#delete(String table, String whereClause, String[] whereArgs)
*/
public synchronized int delete(String table, String whereClause, String[] whereArgs) {
int result = getDatabase().delete(table, whereClause, whereArgs);
onDatabaseUpdated();
return result;
}
public synchronized 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 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.info("Upgrading database from version {} to {}.", oldVersion, newVersion);
database = db;
try {
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);
}
} catch (Exception e) {
log.error("database-upgrade-{}-{}-{}", getName(), oldVersion, newVersion, e);
}
}
}
/**
* Visitor that returns SQL constructor for this property
*
* @author Tim Su <tim@todoroo.com>
*
*/
public static class SqlConstructorVisitor implements PropertyVisitor<String, Void> {
@Override
public String visitInteger(Property<Integer> property, Void data) {
return String.format("%s INTEGER", property.getColumnName());
}
@Override
public String visitLong(Property<Long> property, Void data) {
return String.format("%s INTEGER", property.getColumnName());
}
@Override
public String visitString(Property<String> property, Void data) {
return String.format("%s TEXT", property.getColumnName());
}
}
}

@ -10,6 +10,7 @@ import android.database.Cursor;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.dao.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -19,7 +20,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* DAO for reading data from an instance of {@link AbstractDatabase}. If you
* DAO for reading data from an instance of {@link Database}. If you
* are writing an add-on for Astrid, you probably want to be using a subclass
* of ContentResolverDao instead.
*
@ -34,9 +35,9 @@ public class DatabaseDao<TYPE extends AbstractModel> {
private Table table;
private AbstractDatabase database;
private Database database;
public DatabaseDao(AbstractDatabase database, Class<TYPE> modelClass) {
public DatabaseDao(Database database, Class<TYPE> modelClass) {
this.modelClass = modelClass;
this.database = database;
table = database.getTable(this.modelClass);

@ -0,0 +1,26 @@
package com.todoroo.andlib.data;
/**
* Visitor that returns SQL constructor for this property
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class SqlConstructorVisitor implements Property.PropertyVisitor<String, Void> {
@Override
public String visitInteger(Property<Integer> property, Void data) {
return String.format("%s INTEGER", property.getColumnName());
}
@Override
public String visitLong(Property<Long> property, Void data) {
return String.format("%s INTEGER", property.getColumnName());
}
@Override
public String visitString(Property<String> property, Void data) {
return String.format("%s TEXT", property.getColumnName());
}
}

@ -480,7 +480,7 @@ public class CustomFilterActivity extends InjectingActionBarActivity {
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
Cursor cursor = database.getDatabase().rawQuery(sql.toString(), null);
Cursor cursor = database.rawQuery(sql.toString());
try {
cursor.moveToNext();
instance.start = last == -1 ? cursor.getInt(0) : last;

@ -5,13 +5,20 @@
*/
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.AbstractDatabase;
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;
@ -22,6 +29,9 @@ import com.todoroo.astrid.data.UserActivity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tasks.injection.ForApplication;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -33,65 +43,42 @@ import javax.inject.Singleton;
*
*/
@Singleton
public class Database extends AbstractDatabase {
public class Database {
private static final Logger log = LoggerFactory.getLogger(Database.class);
// --- constants
/**
* Database version number. This variable must be updated when database
* tables are updated, as it determines whether a database needs updating.
*/
public static final int VERSION = 35;
/**
* Database name (must be unique)
*/
private static final int VERSION = 35;
private static final String NAME = "database";
/**
* List of table/ If you're adding a new table, add it to this list and
* also make sure that our SQLite helper does the right thing.
*/
public static final Table[] TABLES = new Table[] {
Task.TABLE,
Metadata.TABLE,
StoreObject.TABLE,
TagData.TABLE,
UserActivity.TABLE,
TaskAttachment.TABLE,
TaskListMetadata.TABLE,
private static final Table[] TABLES = new Table[] {
Task.TABLE,
Metadata.TABLE,
StoreObject.TABLE,
TagData.TABLE,
UserActivity.TABLE,
TaskAttachment.TABLE,
TaskListMetadata.TABLE,
};
private final ArrayList<DatabaseUpdateListener> listeners = new ArrayList<>();
private final SQLiteOpenHelper helper;
private SQLiteDatabase database;
// --- listeners
@Inject
public Database() {
public Database(@ForApplication Context context) {
helper = new DatabaseHelper(context, getName(), VERSION);
}
// --- implementation
@Override
public String getName() {
return NAME;
}
@Override
protected int getVersion() {
return VERSION;
}
@Override
public Table[] getTables() {
return TABLES;
}
/**
* Create indices
*/
@Override
protected synchronized void onCreateTables() {
private void onCreateTables() {
StringBuilder sql = new StringBuilder();
sql.append("CREATE INDEX IF NOT EXISTS md_tid ON ").
append(Metadata.TABLE).append('(').
@ -124,8 +111,7 @@ public class Database extends AbstractDatabase {
sql.setLength(0);
}
@Override
protected synchronized boolean onUpgrade(int oldVersion, int newVersion) {
private boolean onUpgrade(int oldVersion, int newVersion) {
SqlConstructorVisitor visitor = new SqlConstructorVisitor();
switch(oldVersion) {
}
@ -153,7 +139,7 @@ public class Database extends AbstractDatabase {
return builder.toString();
}
public void tryAddColumn(Table table, Property<?> column, String defaultValue) {
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$
@ -168,10 +154,7 @@ public class Database extends AbstractDatabase {
}
}
/**
* Create table generation SQL
*/
public String createTableSql(SqlConstructorVisitor visitor,
private String createTableSql(SqlConstructorVisitor visitor,
String tableName, Property<?>[] properties) {
StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE IF NOT EXISTS ").append(tableName).append('(').
@ -186,5 +169,186 @@ public class Database extends AbstractDatabase {
return sql.toString();
}
public void addListener(DatabaseUpdateListener listener) {
listeners.add(listener);
}
private void onDatabaseUpdated() {
for(DatabaseUpdateListener listener : listeners) {
listener.onDatabaseUpdated();
}
}
/**
* 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) {
log.error(e.getMessage(), e);
throw new IllegalStateException(e);
} catch (final RuntimeException original) {
log.error(original.getMessage(), original);
try {
// provide read-only database
openForReading();
} catch (Exception readException) {
log.error(readException.getMessage(), readException);
// 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
log.error(e.getMessage(), e);
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) {
log.info("Upgrading database from version {} to {}.", 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) {
log.error("database-upgrade-{}-{}-{}", getName(), oldVersion, newVersion, e);
}
}
}
}

@ -0,0 +1,5 @@
package com.todoroo.astrid.dao;
public interface DatabaseUpdateListener {
public void onDatabaseUpdated();
}

@ -1,6 +1,5 @@
package com.todoroo.astrid.dao;
import com.todoroo.andlib.data.AbstractDatabase;
import com.todoroo.andlib.data.DatabaseDao;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.helper.UUIDHelper;
@ -16,7 +15,7 @@ import com.todoroo.astrid.helper.UUIDHelper;
*/
public class RemoteModelDao<RTYPE extends RemoteModel> extends DatabaseDao<RTYPE> {
public RemoteModelDao(AbstractDatabase database, Class<RTYPE> modelClass) {
public RemoteModelDao(Database database, Class<RTYPE> modelClass) {
super(database, modelClass);
}

@ -10,7 +10,6 @@ import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import com.todoroo.andlib.data.AbstractDatabase;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.dao.Database;
@ -113,7 +112,7 @@ public class SqlContentProvider extends InjectingContentProvider {
return getDatabase().rawQuery(selection);
}
private AbstractDatabase getDatabase() {
private Database getDatabase() {
if (!open) {
database.get().openForWriting();
open = true;

@ -18,23 +18,20 @@ import android.database.sqlite.SQLiteException;
import android.media.AudioManager;
import android.widget.Toast;
import com.todoroo.andlib.data.AbstractDatabase;
import com.todoroo.andlib.data.DatabaseDao.ModelUpdateListener;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.backup.BackupConstants;
import com.todoroo.astrid.backup.TasksXmlImporter;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.DatabaseUpdateListener;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TagDataDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gcal.CalendarAlarmScheduler;
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
@ -69,7 +66,6 @@ public class StartupService {
// --- application startup
private final UpgradeService upgradeService;
private final TaskService taskService;
private final TagDataDao tagDataDao;
private final Database database;
private final GtasksPreferenceService gtasksPreferenceService;
@ -82,15 +78,13 @@ public class StartupService {
private Broadcaster broadcaster;
@Inject
public StartupService(UpgradeService upgradeService, TaskService taskService,
TagDataDao tagDataDao, Database database,
public StartupService(UpgradeService upgradeService, TagDataDao tagDataDao, Database database,
GtasksPreferenceService gtasksPreferenceService,
GtasksSyncService gtasksSyncService, MetadataDao metadataDao,
Preferences preferences, TasksXmlImporter xmlImporter,
CalendarAlarmScheduler calendarAlarmScheduler, TaskDeleter taskDeleter,
Broadcaster broadcaster) {
this.upgradeService = upgradeService;
this.taskService = taskService;
this.tagDataDao = tagDataDao;
this.database = database;
this.gtasksPreferenceService = gtasksPreferenceService;
@ -117,7 +111,7 @@ public class StartupService {
// sets up activity manager
ContextManager.setContext(activity);
database.addListener(new AbstractDatabase.DatabaseUpdateListener() {
database.addListener(new DatabaseUpdateListener() {
@Override
public void onDatabaseUpdated() {
Astrid2TaskProvider.notifyDatabaseModification(activity);
@ -127,7 +121,6 @@ public class StartupService {
try {
database.openForWriting();
checkForMissingColumns();
} catch (SQLiteException e) {
handleSQLiteError(activity, e);
return;
@ -234,22 +227,6 @@ public class StartupService {
DialogUtilities.okDialog(activity, activity.getString(R.string.DB_corrupted_title), 0, activity.getString(R.string.DB_corrupted_body));
}
private void checkForMissingColumns() {
// For some reason these properties are missing for some users.
// Make them exist!
try {
TodorooCursor<Task> tasks = taskService.query(Query.select(Task.UUID).limit(1));
try {
System.err.println(tasks.getCount());
} finally {
tasks.close();
}
} catch (SQLiteException e) {
log.error(e.getMessage(), e);
database.tryAddColumn(Task.TABLE, Task.UUID, "'0'"); //$NON-NLS-1$
}
}
/**
* If database exists, no tasks but metadata, and a backup file exists, restore it
*/

Loading…
Cancel
Save