388 lines
13 KiB
Java
388 lines
13 KiB
Java
/*
|
|
* Copyright (c) 2015-present, Parse, LLC.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
package com.parse;
|
|
|
|
import android.content.ContentValues;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
|
|
import bolts.Continuation;
|
|
import bolts.Task;
|
|
import bolts.TaskCompletionSource;
|
|
|
|
/** package */ class ParseSQLiteDatabase {
|
|
|
|
/**
|
|
* Database connections are locked to the thread that they are created in when using transactions.
|
|
* We must use a single thread executor to make sure that all transactional DB actions are on
|
|
* the same thread or else they will block.
|
|
*
|
|
* Symptoms include blocking on db.query, cursor.moveToFirst, etc.
|
|
*/
|
|
private static final ExecutorService dbExecutor = Executors.newSingleThreadExecutor();
|
|
|
|
/**
|
|
* Queue for all database sessions. All database sessions must be serialized in order for
|
|
* transactions to work correctly.
|
|
*/
|
|
//TODO (grantland): do we have to serialize sessions of different databases?
|
|
private static final TaskQueue taskQueue = new TaskQueue();
|
|
|
|
/* protected */ static Task<ParseSQLiteDatabase> openDatabaseAsync(final SQLiteOpenHelper helper, int flags) {
|
|
final ParseSQLiteDatabase db = new ParseSQLiteDatabase(flags);
|
|
return db.open(helper).continueWithTask(new Continuation<Void, Task<ParseSQLiteDatabase>>() {
|
|
@Override
|
|
public Task<ParseSQLiteDatabase> then(Task<Void> task) throws Exception {
|
|
return Task.forResult(db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private SQLiteDatabase db;
|
|
private Task<Void> current = null;
|
|
private final Object currentLock = new Object();
|
|
private final TaskCompletionSource<Void> tcs = new TaskCompletionSource<>();
|
|
|
|
private int openFlags;
|
|
|
|
/**
|
|
* Creates a Session which opens a database connection and begins a transaction
|
|
*/
|
|
private ParseSQLiteDatabase(int flags) {
|
|
//TODO (grantland): if (!writable) -- disable transactions?
|
|
//TODO (grantland): if (!writable) -- do we have to serialize everything?
|
|
openFlags = flags;
|
|
|
|
taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> toAwait) throws Exception {
|
|
synchronized (currentLock) {
|
|
current = toAwait;
|
|
}
|
|
return tcs.getTask();
|
|
}
|
|
});
|
|
}
|
|
|
|
public Task<Boolean> isReadOnlyAsync() {
|
|
synchronized (currentLock) {
|
|
Task<Boolean> task = current.continueWith(new Continuation<Void, Boolean>() {
|
|
@Override
|
|
public Boolean then(Task<Void> task) throws Exception {
|
|
return db.isReadOnly();
|
|
}
|
|
});
|
|
current = task.makeVoid();
|
|
return task;
|
|
}
|
|
}
|
|
|
|
public Task<Boolean> isOpenAsync() {
|
|
synchronized (currentLock) {
|
|
Task<Boolean> task = current.continueWith(new Continuation<Void, Boolean>() {
|
|
@Override
|
|
public Boolean then(Task<Void> task) throws Exception {
|
|
return db.isOpen();
|
|
}
|
|
});
|
|
current = task.makeVoid();
|
|
return task;
|
|
}
|
|
}
|
|
|
|
public boolean inTransaction() {
|
|
return db.inTransaction();
|
|
}
|
|
|
|
/* package */ Task<Void> open(final SQLiteOpenHelper helper) {
|
|
synchronized (currentLock) {
|
|
current = current.continueWith(new Continuation<Void, SQLiteDatabase>() {
|
|
@Override
|
|
public SQLiteDatabase then(Task<Void> task) throws Exception {
|
|
// get*Database() is synchronous and calls through SQLiteOpenHelper#onCreate, onUpdate,
|
|
// etc.
|
|
return (openFlags & SQLiteDatabase.OPEN_READONLY) == SQLiteDatabase.OPEN_READONLY
|
|
? helper.getReadableDatabase()
|
|
: helper.getWritableDatabase();
|
|
}
|
|
}, dbExecutor).continueWithTask(new Continuation<SQLiteDatabase, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<SQLiteDatabase> task) throws Exception {
|
|
db = task.getResult();
|
|
return task.makeVoid();
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR); // We want to jump off the dbExecutor
|
|
return current;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a BEGIN TRANSACTION.
|
|
* @see SQLiteDatabase#beginTransaction
|
|
*/
|
|
public Task<Void> beginTransactionAsync() {
|
|
synchronized (currentLock) {
|
|
current = current.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
db.beginTransaction();
|
|
return task;
|
|
}
|
|
}, dbExecutor);
|
|
return current.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a transaction as successful.
|
|
* @see SQLiteDatabase#setTransactionSuccessful
|
|
*/
|
|
public Task<Void> setTransactionSuccessfulAsync() {
|
|
synchronized (currentLock) {
|
|
current = current.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
db.setTransactionSuccessful();
|
|
return task;
|
|
}
|
|
}, dbExecutor);
|
|
return current.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ends a transaction.
|
|
* @see SQLiteDatabase#endTransaction
|
|
*/
|
|
public Task<Void> endTransactionAsync() {
|
|
synchronized (currentLock) {
|
|
current = current.continueWith(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
db.endTransaction();
|
|
// We want to swallow any exceptions from our Session task
|
|
return null;
|
|
}
|
|
}, dbExecutor);
|
|
return current.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes this session, sets the transaction as successful if no errors occurred, ends the
|
|
* transaction and closes the database connection.
|
|
*/
|
|
public Task<Void> closeAsync() {
|
|
synchronized (currentLock) {
|
|
current = current.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
try {
|
|
db.close();
|
|
} finally {
|
|
tcs.setResult(null);
|
|
}
|
|
return tcs.getTask();
|
|
}
|
|
}, dbExecutor);
|
|
return current.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs a SELECT query.
|
|
*
|
|
* @see SQLiteDatabase#query
|
|
*/
|
|
public Task<Cursor> queryAsync(final String table, final String[] select, final String where,
|
|
final String[] args) {
|
|
synchronized (currentLock) {
|
|
Task<Cursor> task = current.onSuccess(new Continuation<Void, Cursor>() {
|
|
@Override
|
|
public Cursor then(Task<Void> task) throws Exception {
|
|
return db.query(table, select, where, args, null, null, null);
|
|
}
|
|
}, dbExecutor).onSuccess(new Continuation<Cursor, Cursor>() {
|
|
@Override
|
|
public Cursor then(Task<Cursor> task) throws Exception {
|
|
Cursor cursor = ParseSQLiteCursor.create(task.getResult(), dbExecutor);
|
|
/* Ensure the cursor window is filled on the dbExecutor thread. We need to do this because
|
|
* the cursor cannot be filled from a different thread than it was created on.
|
|
*/
|
|
cursor.getCount();
|
|
return cursor;
|
|
}
|
|
}, dbExecutor);
|
|
current = task.makeVoid();
|
|
return task.continueWithTask(new Continuation<Cursor, Task<Cursor>>() {
|
|
@Override
|
|
public Task<Cursor> then(Task<Cursor> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes an INSERT.
|
|
* @see SQLiteDatabase#insertWithOnConflict
|
|
*/
|
|
public Task<Void> insertWithOnConflict(final String table, final ContentValues values,
|
|
final int conflictAlgorithm) {
|
|
synchronized (currentLock) {
|
|
Task<Long> task = current.onSuccess(new Continuation<Void, Long>() {
|
|
@Override
|
|
public Long then(Task<Void> task) throws Exception {
|
|
return db.insertWithOnConflict(table, null, values, conflictAlgorithm);
|
|
}
|
|
}, dbExecutor);
|
|
current = task.makeVoid();
|
|
return task.continueWithTask(new Continuation<Long, Task<Long>>() {
|
|
@Override
|
|
public Task<Long> then(Task<Long> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR).makeVoid();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes an INSERT and throws on SQL errors.
|
|
* @see SQLiteDatabase#insertOrThrow
|
|
*/
|
|
public Task<Void> insertOrThrowAsync(final String table, final ContentValues values) {
|
|
synchronized (currentLock) {
|
|
Task<Long> task = current.onSuccess(new Continuation<Void, Long>() {
|
|
@Override
|
|
public Long then(Task<Void> task) throws Exception {
|
|
return db.insertOrThrow(table, null, values);
|
|
}
|
|
}, dbExecutor);
|
|
current = task.makeVoid();
|
|
return task.continueWithTask(new Continuation<Long, Task<Long>>() {
|
|
@Override
|
|
public Task<Long> then(Task<Long> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR).makeVoid();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes an UPDATE.
|
|
* @see SQLiteDatabase#update
|
|
*/
|
|
public Task<Integer> updateAsync(final String table, final ContentValues values,
|
|
final String where, final String[] args) {
|
|
synchronized (currentLock) {
|
|
Task<Integer> task = current.onSuccess(new Continuation<Void, Integer>() {
|
|
@Override
|
|
public Integer then(Task<Void> task) throws Exception {
|
|
return db.update(table, values, where, args);
|
|
}
|
|
}, dbExecutor);
|
|
current = task.makeVoid();
|
|
return task.continueWithTask(new Continuation<Integer, Task<Integer>>() {
|
|
@Override
|
|
public Task<Integer> then(Task<Integer> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a DELETE.
|
|
* @see SQLiteDatabase#delete
|
|
*/
|
|
public Task<Void> deleteAsync(final String table, final String where, final String[] args) {
|
|
synchronized (currentLock) {
|
|
Task<Integer> task = current.onSuccess(new Continuation<Void, Integer>() {
|
|
@Override
|
|
public Integer then(Task<Void> task) throws Exception {
|
|
return db.delete(table, where, args);
|
|
}
|
|
}, dbExecutor);
|
|
current = task.makeVoid();
|
|
return task.continueWithTask(new Continuation<Integer, Task<Integer>>() {
|
|
@Override
|
|
public Task<Integer> then(Task<Integer> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR).makeVoid();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs a raw query.
|
|
*
|
|
* @see SQLiteDatabase#rawQuery
|
|
*/
|
|
public Task<Cursor> rawQueryAsync(final String sql, final String[] args) {
|
|
synchronized (currentLock) {
|
|
Task<Cursor> task = current.onSuccess(new Continuation<Void, Cursor>() {
|
|
@Override
|
|
public Cursor then(Task<Void> task) throws Exception {
|
|
return db.rawQuery(sql, args);
|
|
}
|
|
}, dbExecutor).onSuccess(new Continuation<Cursor, Cursor>() {
|
|
@Override
|
|
public Cursor then(Task<Cursor> task) throws Exception {
|
|
Cursor cursor = ParseSQLiteCursor.create(task.getResult(), dbExecutor);
|
|
// Ensure the cursor window is filled on the dbExecutor thread. We need to do this because
|
|
// the cursor cannot be filled from a different thread than it was created on.
|
|
cursor.getCount();
|
|
return cursor;
|
|
}
|
|
}, dbExecutor);
|
|
current = task.makeVoid();
|
|
return task.continueWithTask(new Continuation<Cursor, Task<Cursor>>() {
|
|
@Override
|
|
public Task<Cursor> then(Task<Cursor> task) throws Exception {
|
|
// We want to jump off the dbExecutor
|
|
return task;
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
}
|
|
}
|