Merge "Add AutoClosingRoomOpenHelper to support AutoClosing room databases." into androidx-main
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperFactoryTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperFactoryTest.kt
new file mode 100644
index 0000000..febc98c
--- /dev/null
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperFactoryTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 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.Context
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+
+public class AutoClosingRoomOpenHelperFactoryTest {
+ private val DB_NAME = "name"
+
+ @Before
+ public fun setUp() {
+ ApplicationProvider.getApplicationContext<Context>().deleteDatabase(DB_NAME)
+ }
+
+ private fun getAutoClosingRoomOpenHelperFactory(
+ timeoutMillis: Long = 10
+ ): AutoClosingRoomOpenHelperFactory {
+ val delegateOpenHelperFactory = FrameworkSQLiteOpenHelperFactory()
+
+ return AutoClosingRoomOpenHelperFactory(
+ delegateOpenHelperFactory,
+ AutoCloser(timeoutMillis, TimeUnit.MILLISECONDS, Executors.newSingleThreadExecutor())
+ )
+ }
+
+ @Test
+ public fun testCallbacksCalled() {
+ val autoClosingRoomOpenHelperFactory =
+ getAutoClosingRoomOpenHelperFactory()
+
+ val callbackCount = AtomicInteger()
+
+ val countingCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ callbackCount.incrementAndGet()
+ }
+
+ override fun onConfigure(db: SupportSQLiteDatabase) {
+ callbackCount.incrementAndGet()
+ }
+
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ callbackCount.incrementAndGet()
+ }
+
+ override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
+ }
+ }
+
+ val autoClosingRoomOpenHelper = autoClosingRoomOpenHelperFactory.create(
+ SupportSQLiteOpenHelper.Configuration
+ .builder(ApplicationProvider.getApplicationContext())
+ .callback(countingCallback)
+ .name(DB_NAME)
+ .build()
+ )
+
+ autoClosingRoomOpenHelper.writableDatabase
+
+ assertEquals(3, callbackCount.get())
+ Thread.sleep(100)
+
+ autoClosingRoomOpenHelper.writableDatabase
+ assertEquals(5, callbackCount.get()) // onCreate won't be called the second time.
+ }
+
+ @Test
+ public fun testDatabaseIsOpenForSlowCallbacks() {
+ val autoClosingRoomOpenHelperFactory =
+ getAutoClosingRoomOpenHelperFactory()
+
+ val refCountCheckingCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ Thread.sleep(100)
+ db.execSQL("create table user (idk int)")
+ }
+
+ override fun onConfigure(db: SupportSQLiteDatabase) {
+ Thread.sleep(100)
+ db.maximumSize = 100000
+ }
+
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ Thread.sleep(100)
+ db.execSQL("select * from user")
+ }
+
+ override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
+ }
+ }
+
+ val autoClosingRoomOpenHelper = autoClosingRoomOpenHelperFactory.create(
+ SupportSQLiteOpenHelper.Configuration
+ .builder(ApplicationProvider.getApplicationContext())
+ .callback(refCountCheckingCallback)
+ .name(DB_NAME)
+ .build()
+ )
+
+ val db = autoClosingRoomOpenHelper.writableDatabase
+ assertTrue(db.isOpen)
+ }
+}
\ No newline at end of file
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperTest.kt
new file mode 100644
index 0000000..e19a845
--- /dev/null
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperTest.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright 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.Context
+import android.database.sqlite.SQLiteException
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.test.core.app.ApplicationProvider
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import java.io.IOException
+import java.lang.UnsupportedOperationException
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+public class AutoClosingRoomOpenHelperTest {
+
+ private open class Callback(var throwOnOpen: Boolean = false) :
+ SupportSQLiteOpenHelper.Callback(1) {
+ override fun onCreate(db: SupportSQLiteDatabase) {}
+
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ if (throwOnOpen) {
+ throw IOException()
+ }
+ }
+
+ override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {}
+ }
+
+ @Before
+ public fun setUp() {
+ ApplicationProvider.getApplicationContext<Context>().deleteDatabase("name")
+ }
+
+ private fun getAutoClosingRoomOpenHelper(
+ timeoutMillis: Long = 10,
+ callback: SupportSQLiteOpenHelper.Callback = Callback()
+ ): AutoClosingRoomOpenHelper {
+
+ val delegateOpenHelper = FrameworkSQLiteOpenHelperFactory()
+ .create(
+ SupportSQLiteOpenHelper.Configuration
+ .builder(ApplicationProvider.getApplicationContext())
+ .callback(callback)
+ .name("name")
+ .build()
+ )
+
+ val autoCloseExecutor = Executors.newSingleThreadExecutor()
+
+ return AutoClosingRoomOpenHelper(
+ delegateOpenHelper,
+ AutoCloser(timeoutMillis, TimeUnit.MILLISECONDS, autoCloseExecutor)
+ )
+ }
+
+ @Test
+ public fun testQueryFailureDecrementsRefCount() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+
+ assertThrows<SQLiteException> {
+ autoClosingRoomOpenHelper
+ .writableDatabase.query("select * from nonexistanttable")
+ }
+
+ assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(0)
+ }
+
+ @Test
+ public fun testCursorKeepsDbAlive() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+ autoClosingRoomOpenHelper.writableDatabase.execSQL("create table user (idk int)")
+
+ val cursor =
+ autoClosingRoomOpenHelper.writableDatabase.query("select * from user")
+ assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(1)
+ cursor.close()
+ assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(0)
+ }
+
+ @Test
+ public fun testTransactionKeepsDbAlive() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+ autoClosingRoomOpenHelper.writableDatabase.beginTransaction()
+ assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(1)
+ autoClosingRoomOpenHelper.writableDatabase.endTransaction()
+ assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(0)
+ }
+
+ @Test
+ public fun enableWriteAheadLogging_onOpenHelper() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+
+ autoClosingRoomOpenHelper.setWriteAheadLoggingEnabled(true)
+ assertThat(autoClosingRoomOpenHelper.writableDatabase.isWriteAheadLoggingEnabled).isTrue()
+
+ Thread.sleep(100) // Let the db auto close...
+
+ assertThat(autoClosingRoomOpenHelper.writableDatabase.isWriteAheadLoggingEnabled).isTrue()
+ }
+
+ @Test
+ public fun testEnableWriteAheadLogging_onSupportSqliteDatabase_throwsUnsupportedOperation() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+
+ assertThrows<UnsupportedOperationException> {
+ autoClosingRoomOpenHelper.writableDatabase.enableWriteAheadLogging()
+ }
+
+ assertThrows<UnsupportedOperationException> {
+ autoClosingRoomOpenHelper.writableDatabase.disableWriteAheadLogging()
+ }
+ }
+
+ @Test
+ public fun testOnOpenCalledOnEachOpen() {
+ val countingCallback = object : Callback() {
+ var onCreateCalls = 0
+ var onOpenCalls = 0
+
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ onCreateCalls++
+ }
+
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ super.onOpen(db)
+ onOpenCalls++
+ }
+ }
+
+ val autoClosingRoomOpenHelper =
+ getAutoClosingRoomOpenHelper(callback = countingCallback)
+
+ autoClosingRoomOpenHelper.writableDatabase
+ assertThat(countingCallback.onOpenCalls).isEqualTo(1)
+ assertThat(countingCallback.onCreateCalls).isEqualTo(1)
+
+ Thread.sleep(20) // Database should auto-close here
+ autoClosingRoomOpenHelper.writableDatabase
+ assertThat(countingCallback.onOpenCalls).isEqualTo(2)
+ assertThat(countingCallback.onCreateCalls).isEqualTo(1)
+ }
+
+ @Test
+ public fun testStatementReturnedByCompileStatement_doesntKeepDatabaseOpen() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+
+ val db = autoClosingRoomOpenHelper.writableDatabase
+ db.execSQL("create table user (idk int)")
+
+ db.compileStatement("insert into users (idk) values (1)")
+
+ Thread.sleep(20)
+ assertThat(db.isOpen).isFalse() // db should close
+ assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(0)
+ }
+
+ @Test
+ public fun testStatementReturnedByCompileStatement_reOpensDatabase() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+
+ val db = autoClosingRoomOpenHelper.writableDatabase
+ db.execSQL("create table user (idk int)")
+
+ val statement = db
+ .compileStatement("insert into user (idk) values (1)")
+
+ Thread.sleep(20)
+
+ statement.executeInsert() // This should succeed
+
+ db.query("select * from user").use {
+ assertThat(it.count).isEqualTo(1)
+ }
+
+ assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(0)
+ }
+
+ @Test
+ public fun testStatementReturnedByCompileStatement_worksWithBinds() {
+ val autoClosingRoomOpenHelper = getAutoClosingRoomOpenHelper()
+ val db = autoClosingRoomOpenHelper.writableDatabase
+
+ db.execSQL("create table users (i int, d double, b blob, n int, s string)")
+
+ val statement = db.compileStatement(
+ "insert into users (i, d, b, n, s) values (?,?,?,?,?)"
+ )
+
+ statement.bindString(5, "123")
+ statement.bindLong(1, 123)
+ statement.bindDouble(2, 1.23)
+ statement.bindBlob(3, byteArrayOf(1, 2, 3))
+ statement.bindNull(4)
+
+ statement.executeInsert()
+
+ db.query("select * from users").use {
+ assertThat(it.moveToFirst()).isTrue()
+ assertThat(it.getInt(0)).isEqualTo(123)
+ assertThat(it.getDouble(1)).isWithin(.01).of(1.23)
+
+ assertThat(it.getBlob(2)).isEqualTo(byteArrayOf(1, 2, 3))
+ assertThat(it.isNull(3)).isTrue()
+ assertThat(it.getString(4)).isEqualTo("123")
+ }
+
+ statement.clearBindings()
+ statement.executeInsert() // should insert with nulls
+
+ db.query("select * from users").use {
+ assertThat(it.moveToFirst()).isTrue()
+ it.moveToNext()
+ assertThat(it.isNull(0)).isTrue()
+ assertThat(it.isNull(1)).isTrue()
+ assertThat(it.isNull(2)).isTrue()
+ assertThat(it.isNull(3)).isTrue()
+ assertThat(it.isNull(4)).isTrue()
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java b/room/runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java
new file mode 100644
index 0000000..87d364f
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java
@@ -0,0 +1,868 @@
+/*
+ * 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.annotation.SuppressLint;
+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.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 {
+ @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() {
+ mAutoClosingDb.close();
+ }
+
+ /**
+ * package protected to pass it to invalidation tracker...
+ */
+ @NonNull
+ AutoCloser getAutoCloser() {
+ return this.mAutoCloser;
+ }
+
+ @NonNull
+ SupportSQLiteDatabase getAutoClosingDb() {
+ return this.mAutoClosingDb;
+ }
+
+ /**
+ * 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;
+ });
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ @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.");
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ @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() {
+ //TODO: I might want to handle a manual close call here differently because as it is
+ // implemented right now, if someone calls close() then calls another method here,
+ // it'll automatically reopen the db, which may not be expected. However, having a
+ // single instance here makes this simpler.
+ throw new IllegalStateException("can't be manually closed yet");
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ @Override
+ public void setNotificationUris(@NonNull ContentResolver cr,
+ @NonNull List<Uri> uris) {
+ mDelegate.setNotificationUris(cr, uris);
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ @Override
+ public Uri getNotificationUri() {
+ return mDelegate.getNotificationUri();
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ @Nullable
+ @Override
+ public List<Uri> getNotificationUris() {
+ return mDelegate.getNotificationUris();
+ }
+
+ @Override
+ public boolean getWantsAllOnMoveCalls() {
+ return mDelegate.getWantsAllOnMoveCalls();
+ }
+
+ @SuppressLint("UnsafeNewApiCall")
+ @RequiresApi(api = Build.VERSION_CODES.M)
+ @Override
+ public void setExtras(Bundle extras) {
+ mDelegate.setExtras(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();
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java b/room/runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java
new file mode 100644
index 0000000..004f60a
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 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 androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+/**
+ * Factory class for AutoClosingRoomOpenHelper
+ */
+final class AutoClosingRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
+ @NonNull
+ private final SupportSQLiteOpenHelper.Factory mDelegate;
+
+ @NonNull
+ private final AutoCloser mAutoCloser;
+
+ AutoClosingRoomOpenHelperFactory(
+ @NonNull SupportSQLiteOpenHelper.Factory factory,
+ @NonNull AutoCloser autoCloser) {
+ mDelegate = factory;
+ mAutoCloser = autoCloser;
+ }
+
+ /**
+ * @return AutoClosingRoomOpenHelper instances.
+ */
+ @Override
+ @NonNull
+ public AutoClosingRoomOpenHelper create(
+ @NonNull SupportSQLiteOpenHelper.Configuration configuration) {
+ return new AutoClosingRoomOpenHelper(mDelegate.create(configuration), mAutoCloser);
+ }
+}