FairEmail/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java

870 lines
28 KiB
Java

/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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.
*/
package androidx.room;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.database.SQLException;
import android.database.sqlite.SQLiteTransactionListener;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.arch.core.util.Function;
import androidx.room.util.SneakyThrow;
import androidx.sqlite.db.SupportSQLiteCompat;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.SupportSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* A SupportSQLiteOpenHelper that has autoclose enabled for database connections.
*/
final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
@NonNull
private final SupportSQLiteOpenHelper mDelegateOpenHelper;
@NonNull
private final AutoClosingSupportSQLiteDatabase mAutoClosingDb;
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingRoomOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
@NonNull AutoCloser autoCloser) {
mDelegateOpenHelper = supportSQLiteOpenHelper;
mAutoCloser = autoCloser;
autoCloser.init(mDelegateOpenHelper);
mAutoClosingDb = new AutoClosingSupportSQLiteDatabase(mAutoCloser);
}
@Nullable
@Override
public String getDatabaseName() {
return mDelegateOpenHelper.getDatabaseName();
}
@Override
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void setWriteAheadLoggingEnabled(boolean enabled) {
mDelegateOpenHelper.setWriteAheadLoggingEnabled(enabled);
}
@NonNull
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public SupportSQLiteDatabase getWritableDatabase() {
// Note we don't differentiate between writable db and readable db
// We try to open the db so the open callbacks run
mAutoClosingDb.pokeOpen();
return mAutoClosingDb;
}
@NonNull
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public SupportSQLiteDatabase getReadableDatabase() {
// Note we don't differentiate between writable db and readable db
// We try to open the db so the open callbacks run
mAutoClosingDb.pokeOpen();
return mAutoClosingDb;
}
@Override
public void close() {
try {
mAutoClosingDb.close();
} catch (IOException e) {
SneakyThrow.reThrow(e);
}
}
/**
* package protected to pass it to invalidation tracker...
*/
@NonNull
AutoCloser getAutoCloser() {
return this.mAutoCloser;
}
@NonNull
SupportSQLiteDatabase getAutoClosingDb() {
return this.mAutoClosingDb;
}
@Override
@NonNull
public SupportSQLiteOpenHelper getDelegate() {
return mDelegateOpenHelper;
}
/**
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
*/
static final class AutoClosingSupportSQLiteDatabase implements SupportSQLiteDatabase {
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingSupportSQLiteDatabase(@NonNull AutoCloser autoCloser) {
mAutoCloser = autoCloser;
}
void pokeOpen() {
mAutoCloser.executeRefCountingFunction(db -> null);
}
@Override
public SupportSQLiteStatement compileStatement(String sql) {
return new AutoClosingSupportSqliteStatement(sql, mAutoCloser);
}
@Override
public void beginTransaction() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransaction();
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionNonExclusive() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionNonExclusive();
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionWithListener(transactionListener);
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionWithListenerNonExclusive(
SQLiteTransactionListener transactionListener) {
// We assume that after every successful beginTransaction() call there *will* always
// be a corresponding endTransaction() call. Without a corresponding
// endTransactionCall we will never close the db.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionWithListenerNonExclusive(transactionListener);
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void endTransaction() {
if (mAutoCloser.getDelegateDatabase() == null) {
// This should never happen.
throw new IllegalStateException("End transaction called but delegateDb is null");
}
try {
mAutoCloser.getDelegateDatabase().endTransaction();
} finally {
mAutoCloser.decrementCountAndScheduleClose();
}
}
@Override
public void setTransactionSuccessful() {
SupportSQLiteDatabase delegate = mAutoCloser.getDelegateDatabase();
if (delegate == null) {
// This should never happen.
throw new IllegalStateException("setTransactionSuccessful called but delegateDb "
+ "is null");
}
delegate.setTransactionSuccessful();
}
@Override
public boolean inTransaction() {
if (mAutoCloser.getDelegateDatabase() == null) {
return false;
}
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction);
}
@Override
public boolean isDbLockedByCurrentThread() {
if (mAutoCloser.getDelegateDatabase() == null) {
return false;
}
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::isDbLockedByCurrentThread);
}
@Override
public boolean yieldIfContendedSafely() {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely);
}
@Override
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely);
}
@Override
public int getVersion() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getVersion);
}
@Override
public void setVersion(int version) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setVersion(version);
return null;
});
}
@Override
public long getMaximumSize() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getMaximumSize);
}
@Override
public long setMaximumSize(long numBytes) {
return mAutoCloser.executeRefCountingFunction(db -> db.setMaximumSize(numBytes));
}
@Override
public long getPageSize() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPageSize);
}
@Override
public void setPageSize(long numBytes) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setPageSize(numBytes);
return null;
});
}
@Override
public Cursor query(String query) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public Cursor query(String query, Object[] bindArgs) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query, bindArgs);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public Cursor query(SupportSQLiteQuery query) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query, cancellationSignal);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public long insert(String table, int conflictAlgorithm, ContentValues values)
throws SQLException {
return mAutoCloser.executeRefCountingFunction(db -> db.insert(table, conflictAlgorithm,
values));
}
@Override
public int delete(String table, String whereClause, Object[] whereArgs) {
return mAutoCloser.executeRefCountingFunction(
db -> db.delete(table, whereClause, whereArgs));
}
@Override
public int update(String table, int conflictAlgorithm, ContentValues values,
String whereClause, Object[] whereArgs) {
return mAutoCloser.executeRefCountingFunction(db -> db.update(table, conflictAlgorithm,
values, whereClause, whereArgs));
}
@Override
public void execSQL(String sql) throws SQLException {
mAutoCloser.executeRefCountingFunction(db -> {
db.execSQL(sql);
return null;
});
}
@Override
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
mAutoCloser.executeRefCountingFunction(db -> {
db.execSQL(sql, bindArgs);
return null;
});
}
@Override
public boolean isReadOnly() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::isReadOnly);
}
@Override
public boolean isOpen() {
// Get the db without incrementing the reference cause we don't want to open
// the db for an isOpen call.
SupportSQLiteDatabase localDelegate = mAutoCloser.getDelegateDatabase();
if (localDelegate == null) {
return false;
}
return localDelegate.isOpen();
}
@Override
public boolean needUpgrade(int newVersion) {
return mAutoCloser.executeRefCountingFunction(db -> db.needUpgrade(newVersion));
}
@Override
public String getPath() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPath);
}
@Override
public void setLocale(Locale locale) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setLocale(locale);
return null;
});
}
@Override
public void setMaxSqlCacheSize(int cacheSize) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setMaxSqlCacheSize(cacheSize);
return null;
});
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void setForeignKeyConstraintsEnabled(boolean enable) {
mAutoCloser.executeRefCountingFunction(db -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
db.setForeignKeyConstraintsEnabled(enable);
}
return null;
});
}
@Override
public boolean enableWriteAheadLogging() {
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
+ "OpenHelper instead of on the database directly.");
}
@Override
public void disableWriteAheadLogging() {
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
+ "OpenHelper instead of on the database directly.");
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public boolean isWriteAheadLoggingEnabled() {
return mAutoCloser.executeRefCountingFunction(db -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return db.isWriteAheadLoggingEnabled();
}
return false;
});
}
@Override
public List<Pair<String, String>> getAttachedDbs() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getAttachedDbs);
}
@Override
public boolean isDatabaseIntegrityOk() {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::isDatabaseIntegrityOk);
}
@Override
public void close() throws IOException {
mAutoCloser.closeDatabaseIfOpen();
}
}
/**
* We need to keep the db alive until the cursor is closed, so we can't decrement our
* reference count until the cursor is closed. The underlying database will not close until
* this cursor is closed.
*/
private static final class KeepAliveCursor implements Cursor {
private final Cursor mDelegate;
private final AutoCloser mAutoCloser;
KeepAliveCursor(Cursor delegate, AutoCloser autoCloser) {
mDelegate = delegate;
mAutoCloser = autoCloser;
}
// close is the only important/changed method here:
@Override
public void close() {
mDelegate.close();
mAutoCloser.decrementCountAndScheduleClose();
}
@Override
public boolean isClosed() {
return mDelegate.isClosed();
}
@Override
public int getCount() {
return mDelegate.getCount();
}
@Override
public int getPosition() {
return mDelegate.getPosition();
}
@Override
public boolean move(int offset) {
return mDelegate.move(offset);
}
@Override
public boolean moveToPosition(int position) {
return mDelegate.moveToPosition(position);
}
@Override
public boolean moveToFirst() {
return mDelegate.moveToFirst();
}
@Override
public boolean moveToLast() {
return mDelegate.moveToLast();
}
@Override
public boolean moveToNext() {
return mDelegate.moveToNext();
}
@Override
public boolean moveToPrevious() {
return mDelegate.moveToPrevious();
}
@Override
public boolean isFirst() {
return mDelegate.isFirst();
}
@Override
public boolean isLast() {
return mDelegate.isLast();
}
@Override
public boolean isBeforeFirst() {
return mDelegate.isBeforeFirst();
}
@Override
public boolean isAfterLast() {
return mDelegate.isAfterLast();
}
@Override
public int getColumnIndex(String columnName) {
return mDelegate.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
return mDelegate.getColumnIndexOrThrow(columnName);
}
@Override
public String getColumnName(int columnIndex) {
return mDelegate.getColumnName(columnIndex);
}
@Override
public String[] getColumnNames() {
return mDelegate.getColumnNames();
}
@Override
public int getColumnCount() {
return mDelegate.getColumnCount();
}
@Override
public byte[] getBlob(int columnIndex) {
return mDelegate.getBlob(columnIndex);
}
@Override
public String getString(int columnIndex) {
return mDelegate.getString(columnIndex);
}
@Override
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
mDelegate.copyStringToBuffer(columnIndex, buffer);
}
@Override
public short getShort(int columnIndex) {
return mDelegate.getShort(columnIndex);
}
@Override
public int getInt(int columnIndex) {
return mDelegate.getInt(columnIndex);
}
@Override
public long getLong(int columnIndex) {
return mDelegate.getLong(columnIndex);
}
@Override
public float getFloat(int columnIndex) {
return mDelegate.getFloat(columnIndex);
}
@Override
public double getDouble(int columnIndex) {
return mDelegate.getDouble(columnIndex);
}
@Override
public int getType(int columnIndex) {
return mDelegate.getType(columnIndex);
}
@Override
public boolean isNull(int columnIndex) {
return mDelegate.isNull(columnIndex);
}
/**
* @deprecated see Cursor.deactivate
*/
@Override
@Deprecated
public void deactivate() {
mDelegate.deactivate();
}
/**
* @deprecated see Cursor.requery
*/
@Override
@Deprecated
public boolean requery() {
return mDelegate.requery();
}
@Override
public void registerContentObserver(ContentObserver observer) {
mDelegate.registerContentObserver(observer);
}
@Override
public void unregisterContentObserver(ContentObserver observer) {
mDelegate.unregisterContentObserver(observer);
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
mDelegate.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
mDelegate.unregisterDataSetObserver(observer);
}
@Override
public void setNotificationUri(ContentResolver cr, Uri uri) {
mDelegate.setNotificationUri(cr, uri);
}
@RequiresApi(api = Build.VERSION_CODES.Q)
@Override
public void setNotificationUris(@NonNull ContentResolver cr,
@NonNull List<Uri> uris) {
SupportSQLiteCompat.Api29Impl.setNotificationUris(mDelegate, cr, uris);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public Uri getNotificationUri() {
return SupportSQLiteCompat.Api19Impl.getNotificationUri(mDelegate);
}
@RequiresApi(api = Build.VERSION_CODES.Q)
@Nullable
@Override
public List<Uri> getNotificationUris() {
return SupportSQLiteCompat.Api29Impl.getNotificationUris(mDelegate);
}
@Override
public boolean getWantsAllOnMoveCalls() {
return mDelegate.getWantsAllOnMoveCalls();
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void setExtras(Bundle extras) {
SupportSQLiteCompat.Api23Impl.setExtras(mDelegate, extras);
}
@Override
public Bundle getExtras() {
return mDelegate.getExtras();
}
@Override
public Bundle respond(Bundle extras) {
return mDelegate.respond(extras);
}
}
/**
* We can't close our db if the SupportSqliteStatement is open.
*
* Each of these that are created need to be registered with RefCounter.
*
* On auto-close, RefCounter needs to close each of these before closing the db that these
* were constructed from.
*
* Each of the methods here need to get
*/
//TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications
// are for the way it's done here, but caching the prepared statement would definitely be more
// complicated since we need to invalidate any of the PreparedStatements that were created
// with this db
private static class AutoClosingSupportSqliteStatement implements SupportSQLiteStatement {
private final String mSql;
private final ArrayList<Object> mBinds = new ArrayList<>();
private final AutoCloser mAutoCloser;
AutoClosingSupportSqliteStatement(
String sql, AutoCloser autoCloser) {
mSql = sql;
mAutoCloser = autoCloser;
}
private <T> T executeSqliteStatementWithRefCount(Function<SupportSQLiteStatement, T> func) {
return mAutoCloser.executeRefCountingFunction(
db -> {
SupportSQLiteStatement statement = db.compileStatement(mSql);
doBinds(statement);
return func.apply(statement);
}
);
}
private void doBinds(SupportSQLiteStatement supportSQLiteStatement) {
// Replay the binds
for (int i = 0; i < mBinds.size(); i++) {
int bindIndex = i + 1; // Bind indices are 1 based so we start at 1 not 0
Object bind = mBinds.get(i);
if (bind == null) {
supportSQLiteStatement.bindNull(bindIndex);
} else if (bind instanceof Long) {
supportSQLiteStatement.bindLong(bindIndex, (Long) bind);
} else if (bind instanceof Double) {
supportSQLiteStatement.bindDouble(bindIndex, (Double) bind);
} else if (bind instanceof String) {
supportSQLiteStatement.bindString(bindIndex, (String) bind);
} else if (bind instanceof byte[]) {
supportSQLiteStatement.bindBlob(bindIndex, (byte[]) bind);
}
}
}
private void saveBinds(int bindIndex, Object value) {
int index = bindIndex - 1;
if (index >= mBinds.size()) {
// Add null entries to the list until we have the desired # of indices
for (int i = mBinds.size(); i <= index; i++) {
mBinds.add(null);
}
}
mBinds.set(index, value);
}
@Override
public void close() throws IOException {
// Nothing to do here since we re-compile the statement each time.
}
@Override
public void execute() {
executeSqliteStatementWithRefCount(statement -> {
statement.execute();
return null;
});
}
@Override
public int executeUpdateDelete() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeUpdateDelete);
}
@Override
public long executeInsert() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeInsert);
}
@Override
public long simpleQueryForLong() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForLong);
}
@Override
public String simpleQueryForString() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForString);
}
@Override
public void bindNull(int index) {
saveBinds(index, null);
}
@Override
public void bindLong(int index, long value) {
saveBinds(index, value);
}
@Override
public void bindDouble(int index, double value) {
saveBinds(index, value);
}
@Override
public void bindString(int index, String value) {
saveBinds(index, value);
}
@Override
public void bindBlob(int index, byte[] value) {
saveBinds(index, value);
}
@Override
public void clearBindings() {
mBinds.clear();
}
}
}