/* * 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 openDatabaseAsync(final SQLiteOpenHelper helper, int flags) { final ParseSQLiteDatabase db = new ParseSQLiteDatabase(flags); return db.open(helper).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return Task.forResult(db); } }); } private SQLiteDatabase db; private Task current = null; private final Object currentLock = new Object(); private final TaskCompletionSource 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>() { @Override public Task then(Task toAwait) throws Exception { synchronized (currentLock) { current = toAwait; } return tcs.getTask(); } }); } public Task isReadOnlyAsync() { synchronized (currentLock) { Task task = current.continueWith(new Continuation() { @Override public Boolean then(Task task) throws Exception { return db.isReadOnly(); } }); current = task.makeVoid(); return task; } } public Task isOpenAsync() { synchronized (currentLock) { Task task = current.continueWith(new Continuation() { @Override public Boolean then(Task task) throws Exception { return db.isOpen(); } }); current = task.makeVoid(); return task; } } public boolean inTransaction() { return db.inTransaction(); } /* package */ Task open(final SQLiteOpenHelper helper) { synchronized (currentLock) { current = current.continueWith(new Continuation() { @Override public SQLiteDatabase then(Task 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>() { @Override public Task then(Task 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 beginTransactionAsync() { synchronized (currentLock) { current = current.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { db.beginTransaction(); return task; } }, dbExecutor); return current.continueWithTask(new Continuation>() { @Override public Task then(Task 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 setTransactionSuccessfulAsync() { synchronized (currentLock) { current = current.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { db.setTransactionSuccessful(); return task; } }, dbExecutor); return current.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // We want to jump off the dbExecutor return task; } }, Task.BACKGROUND_EXECUTOR); } } /** * Ends a transaction. * @see SQLiteDatabase#endTransaction */ public Task endTransactionAsync() { synchronized (currentLock) { current = current.continueWith(new Continuation() { @Override public Void then(Task task) throws Exception { db.endTransaction(); // We want to swallow any exceptions from our Session task return null; } }, dbExecutor); return current.continueWithTask(new Continuation>() { @Override public Task then(Task 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 closeAsync() { synchronized (currentLock) { current = current.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { try { db.close(); } finally { tcs.setResult(null); } return tcs.getTask(); } }, dbExecutor); return current.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // We want to jump off the dbExecutor return task; } }, Task.BACKGROUND_EXECUTOR); } } /** * Runs a SELECT query. * * @see SQLiteDatabase#query */ public Task queryAsync(final String table, final String[] select, final String where, final String[] args) { synchronized (currentLock) { Task task = current.onSuccess(new Continuation() { @Override public Cursor then(Task task) throws Exception { return db.query(table, select, where, args, null, null, null); } }, dbExecutor).onSuccess(new Continuation() { @Override public Cursor then(Task 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>() { @Override public Task then(Task task) throws Exception { // We want to jump off the dbExecutor return task; } }, Task.BACKGROUND_EXECUTOR); } } /** * Executes an INSERT. * @see SQLiteDatabase#insertWithOnConflict */ public Task insertWithOnConflict(final String table, final ContentValues values, final int conflictAlgorithm) { synchronized (currentLock) { Task task = current.onSuccess(new Continuation() { @Override public Long then(Task task) throws Exception { return db.insertWithOnConflict(table, null, values, conflictAlgorithm); } }, dbExecutor); current = task.makeVoid(); return task.continueWithTask(new Continuation>() { @Override public Task then(Task 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 insertOrThrowAsync(final String table, final ContentValues values) { synchronized (currentLock) { Task task = current.onSuccess(new Continuation() { @Override public Long then(Task task) throws Exception { return db.insertOrThrow(table, null, values); } }, dbExecutor); current = task.makeVoid(); return task.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // We want to jump off the dbExecutor return task; } }, Task.BACKGROUND_EXECUTOR).makeVoid(); } } /** * Executes an UPDATE. * @see SQLiteDatabase#update */ public Task updateAsync(final String table, final ContentValues values, final String where, final String[] args) { synchronized (currentLock) { Task task = current.onSuccess(new Continuation() { @Override public Integer then(Task task) throws Exception { return db.update(table, values, where, args); } }, dbExecutor); current = task.makeVoid(); return task.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // We want to jump off the dbExecutor return task; } }, Task.BACKGROUND_EXECUTOR); } } /** * Executes a DELETE. * @see SQLiteDatabase#delete */ public Task deleteAsync(final String table, final String where, final String[] args) { synchronized (currentLock) { Task task = current.onSuccess(new Continuation() { @Override public Integer then(Task task) throws Exception { return db.delete(table, where, args); } }, dbExecutor); current = task.makeVoid(); return task.continueWithTask(new Continuation>() { @Override public Task then(Task 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 rawQueryAsync(final String sql, final String[] args) { synchronized (currentLock) { Task task = current.onSuccess(new Continuation() { @Override public Cursor then(Task task) throws Exception { return db.rawQuery(sql, args); } }, dbExecutor).onSuccess(new Continuation() { @Override public Cursor then(Task 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>() { @Override public Task then(Task task) throws Exception { // We want to jump off the dbExecutor return task; } }, Task.BACKGROUND_EXECUTOR); } } }