Merge "Implement the package-private AutoCloser which is responsible for reference counting, and scheduling auto-close operations." into androidx-master-dev
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
index 0a0769d..fbce4b2 100644
--- a/room/runtime/build.gradle
+++ b/room/runtime/build.gradle
@@ -55,8 +55,12 @@
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ESPRESSO_CORE)
+    androidTestImplementation(KOTLIN_STDLIB)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(project(":internal-testutils-truth")) // for assertThrows
+    androidTestImplementation("androidx.arch.core:core-testing:2.0.1")
+
 }
 
 android.libraryVariants.all { variant ->
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
new file mode 100644
index 0000000..87e9c17
--- /dev/null
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+public class AutoCloserTest {
+
+    @get:Rule
+    public val countingTaskExecutorRule: CountingTaskExecutorRule = CountingTaskExecutorRule()
+
+    private lateinit var autoCloser: AutoCloser
+
+    private lateinit var callback: Callback
+
+    private 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() {
+        callback = Callback()
+
+        val delegateOpenHelper = FrameworkSQLiteOpenHelperFactory()
+            .create(
+                SupportSQLiteOpenHelper.Configuration
+                    .builder(ApplicationProvider.getApplicationContext())
+                    .callback(callback)
+                    .name("name")
+                    .build()
+            )
+
+        val autoCloseExecutor = ArchTaskExecutor.getIOThreadExecutor()
+
+        autoCloser = AutoCloser(
+            1,
+            TimeUnit.MILLISECONDS,
+            autoCloseExecutor
+        ).also {
+            it.init(delegateOpenHelper)
+        }
+    }
+
+    @After
+    public fun cleanUp() {
+        assertThat(countingTaskExecutorRule.isIdle).isTrue()
+    }
+
+    @Test
+    public fun refCountsCounted() {
+        autoCloser.incrementCountAndEnsureDbIsOpen()
+        assertThat(autoCloser.refCountForTest).isEqualTo(1)
+
+        autoCloser.incrementCountAndEnsureDbIsOpen()
+        assertThat(autoCloser.refCountForTest).isEqualTo(2)
+
+        autoCloser.decrementCountAndScheduleClose()
+        assertThat(autoCloser.refCountForTest).isEqualTo(1)
+
+        autoCloser.executeRefCountingFunction {
+            assertThat(autoCloser.refCountForTest).isEqualTo(2)
+        }
+        assertThat(autoCloser.refCountForTest).isEqualTo(1)
+
+        autoCloser.decrementCountAndScheduleClose()
+        assertThat(autoCloser.refCountForTest).isEqualTo(0)
+
+        // TODO(rohitsat): remove these sleeps and add a hook in AutoCloser to confirm that the
+        // scheduled tasks are done.
+        Thread.sleep(5)
+        countingTaskExecutorRule.drainTasks(10, TimeUnit.MILLISECONDS)
+    }
+
+    @Test
+    public fun executeRefCountingFunctionPropagatesFailure() {
+        assertThrows<IOException> {
+            autoCloser.executeRefCountingFunction {
+                throw IOException()
+            }
+        }
+
+        assertThat(autoCloser.refCountForTest).isEqualTo(0)
+
+        // TODO(rohitsat): remove these sleeps and add a hook in AutoCloser to confirm that the
+        // scheduled tasks are done.
+        Thread.sleep(5)
+        countingTaskExecutorRule.drainTasks(10, TimeUnit.MILLISECONDS)
+    }
+
+    @Test
+    public fun dbNotClosedWithRefCountIncremented() {
+        autoCloser.incrementCountAndEnsureDbIsOpen()
+
+        Thread.sleep(10)
+
+        assertThat(autoCloser.delegateDatabase!!.isOpen).isTrue()
+
+        autoCloser.decrementCountAndScheduleClose()
+
+        // TODO(rohitsat): remove these sleeps and add a hook in AutoCloser to confirm that the
+        // scheduled tasks are done.
+        Thread.sleep(10)
+        assertThat(autoCloser.delegateDatabase).isNull()
+    }
+
+    @Test
+    public fun getDelegatedDatabaseReturnsUnwrappedDatabase() {
+        assertThat(autoCloser.delegateDatabase).isNull()
+
+        val db = autoCloser.incrementCountAndEnsureDbIsOpen()
+        db.beginTransaction()
+        // Beginning a transaction on the unwrapped db shouldn't increment our ref count.
+        assertThat(autoCloser.refCountForTest).isEqualTo(1)
+        db.endTransaction()
+
+        autoCloser.delegateDatabase!!.beginTransaction()
+        assertThat(autoCloser.refCountForTest).isEqualTo(1)
+        autoCloser.delegateDatabase!!.endTransaction()
+        autoCloser.decrementCountAndScheduleClose()
+
+        autoCloser.executeRefCountingFunction {
+            assertThat(autoCloser.refCountForTest).isEqualTo(1)
+        }
+
+        // TODO(rohitsat): remove these sleeps and add a hook in AutoCloser to confirm that the
+        // scheduled tasks are done.
+        Thread.sleep(5)
+        countingTaskExecutorRule.drainTasks(10, TimeUnit.MILLISECONDS)
+    }
+
+    @Test
+    public fun refCountStaysIncrementedWhenErrorIsEncountered() {
+        callback.throwOnOpen = true
+        assertThrows<IOException> {
+            autoCloser.incrementCountAndEnsureDbIsOpen()
+        }
+
+        assertThat(autoCloser.refCountForTest).isEqualTo(1)
+
+        autoCloser.decrementCountAndScheduleClose()
+        callback.throwOnOpen = false
+
+        // TODO(rohitsat): remove these sleeps and add a hook in AutoCloser to confirm that the
+        // scheduled tasks are done.
+        Thread.sleep(5)
+        countingTaskExecutorRule.drainTasks(10, TimeUnit.MILLISECONDS)
+    }
+}
\ No newline at end of file
diff --git a/room/runtime/src/main/java/androidx/room/AutoCloser.java b/room/runtime/src/main/java/androidx/room/AutoCloser.java
new file mode 100644
index 0000000..eda2363
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/AutoCloser.java
@@ -0,0 +1,256 @@
+/*
+ * 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.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.arch.core.util.Function;
+import androidx.room.util.SneakyThrow;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * AutoCloser is responsible for automatically opening (using
+ * delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
+ * SupportSqliteDatabase.
+ *
+ * It is important to ensure that the ref count is incremented when using a returned database.
+ */
+final class AutoCloser {
+
+    @Nullable
+    private SupportSQLiteOpenHelper mDelegateOpenHelper = null;
+
+    @NonNull
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    // Package private for access from mAutoCloser
+    @NonNull
+    final Object mLock = new Object();
+
+    // Package private for access from mAutoCloser
+    final long mAutoCloseTimeoutInMs;
+
+    // Package private for access from mExecuteAutoCloser
+    @NonNull
+    final Executor mExecutor;
+
+    // Package private for access from mAutoCloser
+    @GuardedBy("mLock")
+    int mRefCount = 0;
+
+    // Package private for access from mAutoCloser
+    @GuardedBy("mLock")
+    long mLastDecrementRefCountTimeStamp = SystemClock.uptimeMillis();
+
+    // The unwrapped SupportSqliteDatabase
+    // Package private for access from mAutoCloser
+    @GuardedBy("mLock")
+    @Nullable
+    SupportSQLiteDatabase mDelegateDatabase;
+
+    private final Runnable mExecuteAutoCloser = new Runnable() {
+        @Override
+        public void run() {
+            mExecutor.execute(mAutoCloser);
+        }
+    };
+
+    // Package private for access from mExecuteAutoCloser
+    @NonNull
+    final Runnable mAutoCloser = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (mLock) {
+                if (SystemClock.uptimeMillis() - mLastDecrementRefCountTimeStamp
+                        < mAutoCloseTimeoutInMs) {
+                    // An increment + decrement beat us to closing the db. We
+                    // will not close the database, and there should be at least
+                    // one more auto-close scheduled.
+                    return;
+                }
+
+                if (mRefCount != 0) {
+                    // An increment beat us to closing the db. We don't close the
+                    // db, and another closer will be scheduled once the ref
+                    // count is decremented.
+                    return;
+                }
+
+                if (mDelegateDatabase != null) {
+                    try {
+                        mDelegateDatabase.close();
+                    } catch (IOException e) {
+                        SneakyThrow.reThrow(e);
+                    }
+                    mDelegateDatabase = null;
+                }
+            }
+        }
+    };
+
+
+    /**
+     * Construct an AutoCloser.
+     *
+     * @param autoCloseTimeoutAmount time for auto close timer
+     * @param autoCloseTimeUnit      time unit for autoCloseTimeoutAmount
+     * @param autoCloseExecutor      the executor on which the auto close operation will happen
+     */
+    AutoCloser(long autoCloseTimeoutAmount,
+            @NonNull TimeUnit autoCloseTimeUnit,
+            @NonNull Executor autoCloseExecutor) {
+        mAutoCloseTimeoutInMs = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount);
+        mExecutor = autoCloseExecutor;
+    }
+
+    /**
+     * Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
+     * delegateOpenHelper after construction.
+     *
+     * @param delegateOpenHelper the open helper that is used to create
+     *                           new SupportSqliteDatabases
+     */
+    public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) {
+        if (mDelegateOpenHelper != null) {
+            Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. This is probably a bug in"
+                    + " the room code.");
+            return;
+        }
+        this.mDelegateOpenHelper = delegateOpenHelper;
+    }
+
+    /**
+     * Execute a ref counting function. The function will receive an unwrapped open database and
+     * this database will stay open until at least after function returns. If there are no more
+     * references in use for the db once function completes, an auto close operation will be
+     * scheduled.
+     */
+    @Nullable
+    public <V> V executeRefCountingFunction(@NonNull Function<SupportSQLiteDatabase, V> function) {
+        try {
+            SupportSQLiteDatabase db = incrementCountAndEnsureDbIsOpen();
+            return function.apply(db);
+        } finally {
+            decrementCountAndScheduleClose();
+        }
+    }
+
+    /**
+     * Confirms that autoCloser is no longer running and confirms that mDelegateDatabase is set
+     * and open. mDelegateDatabase will not be auto closed until
+     * decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be
+     * called once for each call to incrementCountAndEnsureDbIsOpen.
+     *
+     * If this throws an exception, decrementCountAndScheduleClose must still be called!
+     *
+     * @return the *unwrapped* SupportSQLiteDatabase.
+     */
+    @NonNull
+    public SupportSQLiteDatabase incrementCountAndEnsureDbIsOpen() {
+        //TODO(rohitsat): avoid synchronized(mLock) when possible. We should be able to avoid it
+        // when refCount is not hitting zero or if there is no auto close scheduled if we use
+        // Atomics.
+        synchronized (mLock) {
+            // If there is a scheduled autoclose operation, we should remove it from the handler.
+            mHandler.removeCallbacks(mExecuteAutoCloser);
+
+            mRefCount++;
+
+            if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) {
+                return mDelegateDatabase;
+            } else if (mDelegateDatabase != null) {
+                // This shouldn't happen
+                throw new IllegalStateException("mDelegateDatabase is closed but non-null");
+            }
+
+            // Get the database while holding `mLock` so no other threads try to create it or
+            // destroy it.
+            if (mDelegateOpenHelper != null) {
+                mDelegateDatabase = mDelegateOpenHelper.getWritableDatabase();
+            } else {
+                throw new IllegalStateException("AutoCloser has not beeninitialized. This "
+                        + "shouldn't happen, but if it does it means there's a bug in our code");
+            }
+
+            return mDelegateDatabase;
+        }
+    }
+
+    /**
+     * Decrements the ref count and schedules a close if there are no other references to the db.
+     * This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call.
+     */
+    public void decrementCountAndScheduleClose() {
+        //TODO(rohitsat): avoid synchronized(mLock) when possible
+        synchronized (mLock) {
+            if (mRefCount <= 0) {
+                throw new IllegalStateException("ref count is 0 or lower but we're supposed to "
+                        + "decrement");
+            }
+
+            // decrement refCount
+            mRefCount--;
+
+            // if refcount is zero, schedule close operation
+            if (mRefCount == 0) {
+                if (mDelegateDatabase == null) {
+                    // No db to close, this can happen due to exceptions when creating db...
+                    return;
+                }
+                mHandler.postDelayed(mExecuteAutoCloser, mAutoCloseTimeoutInMs);
+            }
+        }
+    }
+
+    /**
+     * Returns the underlying database. This does not ensure that the database is open; the
+     * caller is responsible for ensuring that the database is open and the ref count is non-zero.
+     *
+     * This is primarily meant for use cases where we don't want to open the database (isOpen) or
+     * we know that the database is already open (KeepAliveCursor).
+     */
+    @Nullable // Since the db might be closed
+    public SupportSQLiteDatabase getDelegateDatabase() {
+        synchronized (mLock) {
+            return mDelegateDatabase;
+        }
+    }
+
+    /**
+     * Returns the current ref count for this auto closer. This is only visible for testing.
+     *
+     * @return current ref count
+     */
+    @VisibleForTesting
+    public int getRefCountForTest() {
+        synchronized (mLock) {
+            return mRefCount;
+        }
+    }
+}