Merge changes from topic "inavlidation-tracker-runtime" into androidx-main
* changes:
Converting `invalidation tracker` related files in `room-runtime` from Java to Kotlin.
Initial code check-in for renaming `invalidation tracker` related files in `room-runtime` from *.java to *.kt.
diff --git a/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt b/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt
index e3552e1..2d19f32 100644
--- a/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt
+++ b/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt
@@ -67,7 +67,7 @@
.build()
val observer = object : InvalidationTracker.Observer("user") {
- override fun onInvalidated(tables: MutableSet<String>) {}
+ override fun onInvalidated(tables: Set<String>) {}
}
db.invalidationTracker.addObserver(observer)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/InvalidationTrackerExt.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/InvalidationTrackerExt.kt
index 22c113d..f254556 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/InvalidationTrackerExt.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/InvalidationTrackerExt.kt
@@ -20,24 +20,12 @@
import java.util.concurrent.TimeUnit
/**
- * Makes refresh runnable accessible in tests
- */
-val InvalidationTracker.refreshRunnable: Runnable
- get() = this.mRefreshRunnable
-
-/**
- * True if invalidation tracker is pending a refresh event to get database changes.
- */
-val InvalidationTracker.pendingRefresh
- get() = this.mPendingRefresh.get()
-
-/**
* Polls [InvalidationTracker] until it sets its pending refresh flag to true.
*/
suspend fun InvalidationTracker.awaitPendingRefresh() {
withTimeout(TimeUnit.SECONDS.toMillis(10)) {
while (true) {
- if (pendingRefresh) return@withTimeout
+ if (pendingRefresh.get()) return@withTimeout
delay(50)
}
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
index bf212a9..0457592 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
@@ -36,8 +36,6 @@
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.awaitPendingRefresh
-import androidx.room.pendingRefresh
-import androidx.room.refreshRunnable
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.test.core.app.ApplicationProvider
@@ -467,7 +465,7 @@
toIndex = 20 + CONFIG.initialLoadSize
)
)
- assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+ assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
// now do some changes in the database but don't let change notifications go through
// to the data source. it should not crash :)
queryExecutor.filterFunction = { runnable ->
@@ -521,7 +519,7 @@
// Runs the original invalidationTracker.refreshRunnable.
// Note that the second initial load's call to mRefreshRunnable resets the flag to
// false, so this mRefreshRunnable will not detect changes in the table anymore.
- assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+ assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
queryExecutor.executeAll()
itemStore.awaitInitialLoad()
@@ -556,7 +554,7 @@
val blockingObserver = object : InvalidationTracker.Observer("PagingEntity") {
// make sure observer blocks the time longer than the timeout of waiting for
// paging source invalidation, so that we can assert new generation failure later
- override fun onInvalidated(tables: MutableSet<String>) {
+ override fun onInvalidated(tables: Set<String>) {
Thread.sleep(3_500)
}
}
@@ -576,7 +574,7 @@
// should load starting from initial Key = 20
initialItems
)
- assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+ assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
db.dao.deleteItems(
items.subList(0, 60).map { it.id }
@@ -643,7 +641,7 @@
toIndex = CONFIG.initialLoadSize
)
)
- assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+ assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
// now do some changes in the database but don't let change notifications go through
// to the data source. it should not crash :)
queryExecutor.filterFunction = { runnable ->
@@ -696,7 +694,7 @@
// Runs the original invalidationTracker.refreshRunnable.
// Note that the second initial load's call to mRefreshRunnable resets the flag to
// false, so this mRefreshRunnable will not detect changes in the table anymore.
- assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+ assertThat(db.invalidationTracker.pendingRefresh.get()).isFalse()
queryExecutor.executeAll()
itemStore.awaitInitialLoad()
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt
index 43da129..0586d72 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt
@@ -142,7 +142,7 @@
val latch = CountDownLatch(expectedInvalidationCount)
- override fun onInvalidated(tables: MutableSet<String>) {
+ override fun onInvalidated(tables: Set<String>) {
latch.countDown()
}
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java
index 13a5469..c57639a 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java
@@ -21,7 +21,7 @@
*/
public class InvalidationTrackerTrojan {
public static int countObservers(InvalidationTracker tracker) {
- return tracker.mObserverMap.size();
+ return tracker.observerMap.size();
}
private InvalidationTrackerTrojan() {
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index bf73c08..2f7b61b 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -22,7 +22,7 @@
}
dependencies {
- api("androidx.annotation:annotation:1.1.0")
+ api("androidx.annotation:annotation:1.3.0")
api(libs.kotlinStdlibJdk8)
testImplementation(libs.junit)
testImplementation(libs.mockitoCore)
diff --git a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt b/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
index 00f0cb4..445b404 100644
--- a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
+++ b/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
@@ -179,7 +179,7 @@
}
}
- private class TestInvalidationTracker(db: RoomDatabase) : InvalidationTracker(db) {
+ private class TestInvalidationTracker(db: RoomDatabase) : InvalidationTracker(db, "") {
val observers = mutableListOf<Observer>()
override fun addObserver(observer: Observer) {
diff --git a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index 27dd4d8..8d0b2a8 100644
--- a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -111,7 +111,7 @@
// Observer channel receives signals from the invalidation tracker to emit queries.
val observerChannel = Channel<Unit>(Channel.CONFLATED)
val observer = object : InvalidationTracker.Observer(tableNames) {
- override fun onInvalidated(tables: MutableSet<String>) {
+ override fun onInvalidated(tables: Set<String>) {
observerChannel.trySend(Unit)
}
}
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/InvalidationTrackerExtRoomPaging.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/InvalidationTrackerExtRoomPaging.kt
index 6ba8c2a..6d83c13 100644
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/InvalidationTrackerExtRoomPaging.kt
+++ b/room/room-paging/src/androidTest/kotlin/androidx/room/InvalidationTrackerExtRoomPaging.kt
@@ -23,14 +23,14 @@
* Makes refresh runnable accessible in tests. Used for LimitOffsetPagingSource unit tests that
* needs to block InvalidationTracker's invalidation notification
*/
-val InvalidationTracker.refreshRunnable: Runnable
- get() = this.mRefreshRunnable
+val InvalidationTracker.refreshRunnableForTest: Runnable
+ get() = this.refreshRunnable
/**
* True if invalidation tracker is pending a refresh event to get database changes.
*/
-val InvalidationTracker.pendingRefresh
- get() = this.mPendingRefresh.get()
+val InvalidationTracker.pendingRefreshForTest
+ get() = this.pendingRefresh.get()
/**
* Polls [InvalidationTracker] until it sets its pending refresh flag to true.
@@ -38,7 +38,7 @@
suspend fun InvalidationTracker.awaitPendingRefresh() {
withTimeout(TimeUnit.SECONDS.toMillis(3)) {
while (true) {
- if (pendingRefresh) return@withTimeout
+ if (pendingRefreshForTest) return@withTimeout
delay(50)
}
}
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index e4991c8..a8795ae 100644
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -27,7 +27,6 @@
import androidx.room.RoomDatabase
import androidx.room.RoomSQLiteQuery
import androidx.room.awaitPendingRefresh
-import androidx.room.refreshRunnable
import androidx.room.util.getColumnIndexOrThrow
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
diff --git a/room/room-paging/src/main/java/androidx/room/paging/util/ThreadSafeInvalidationObserver.kt b/room/room-paging/src/main/java/androidx/room/paging/util/ThreadSafeInvalidationObserver.kt
index 90b9fb7..86c7a76 100644
--- a/room/room-paging/src/main/java/androidx/room/paging/util/ThreadSafeInvalidationObserver.kt
+++ b/room/room-paging/src/main/java/androidx/room/paging/util/ThreadSafeInvalidationObserver.kt
@@ -20,14 +20,15 @@
import androidx.room.RoomDatabase
import java.util.concurrent.atomic.AtomicBoolean
+@Suppress("UNCHECKED_CAST")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ThreadSafeInvalidationObserver(
tables: Array<out String>,
val onInvalidated: () -> Unit,
-) : InvalidationTracker.Observer(tables) {
+) : InvalidationTracker.Observer(tables = tables as Array<String>) {
private val registered: AtomicBoolean = AtomicBoolean(false)
- override fun onInvalidated(tables: MutableSet<String>) {
+ override fun onInvalidated(tables: Set<String>) {
onInvalidated()
}
diff --git a/room/room-runtime/api/current.ignore b/room/room-runtime/api/current.ignore
new file mode 100644
index 0000000..4652540
--- /dev/null
+++ b/room/room-runtime/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+ChangedType: androidx.room.Room#databaseBuilder(android.content.Context, Class<T>, String):
+ Method androidx.room.Room.databaseBuilder has changed return type from androidx.room.RoomDatabase.Builder<T!> to androidx.room.RoomDatabase.Builder<T>
+ChangedType: androidx.room.Room#inMemoryDatabaseBuilder(android.content.Context, Class<T>):
+ Method androidx.room.Room.inMemoryDatabaseBuilder has changed return type from androidx.room.RoomDatabase.Builder<T!> to androidx.room.RoomDatabase.Builder<T>
diff --git a/room/room-runtime/api/current.txt b/room/room-runtime/api/current.txt
index 2d84411..019422f 100644
--- a/room/room-runtime/api/current.txt
+++ b/room/room-runtime/api/current.txt
@@ -25,24 +25,34 @@
}
public class InvalidationTracker {
- method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer);
+ method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer observer);
method public void refreshVersionsAsync();
- method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer);
+ method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer observer);
+ field public static final androidx.room.InvalidationTracker.Companion Companion;
+ }
+
+ public static final class InvalidationTracker.Companion {
}
public abstract static class InvalidationTracker.Observer {
- ctor protected InvalidationTracker.Observer(String, java.lang.String!...);
- ctor public InvalidationTracker.Observer(String![]);
- method public abstract void onInvalidated(java.util.Set<java.lang.String!>);
+ ctor public InvalidationTracker.Observer(String![] tables);
+ ctor protected InvalidationTracker.Observer(String firstTable, java.lang.String... rest);
+ method public abstract void onInvalidated(java.util.Set<java.lang.String> tables);
}
public class Room {
ctor @Deprecated public Room();
- method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T!> databaseBuilder(android.content.Context, Class<T!>, String);
- method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T!> inMemoryDatabaseBuilder(android.content.Context, Class<T!>);
+ method public static final <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context context, Class<T> klass, String? name);
+ method public static final <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context context, Class<T> klass);
+ field public static final androidx.room.Room.Companion Companion;
field public static final String MASTER_TABLE_NAME = "room_master_table";
}
+ public static final class Room.Companion {
+ method public <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context context, Class<T> klass, String? name);
+ method public <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context context, Class<T> klass);
+ }
+
public abstract class RoomDatabase {
ctor public RoomDatabase();
method @Deprecated public void beginTransaction();
diff --git a/room/room-runtime/api/public_plus_experimental_current.txt b/room/room-runtime/api/public_plus_experimental_current.txt
index 3debf0f..695f17f 100644
--- a/room/room-runtime/api/public_plus_experimental_current.txt
+++ b/room/room-runtime/api/public_plus_experimental_current.txt
@@ -28,29 +28,39 @@
}
public class InvalidationTracker {
- method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer);
+ method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer observer);
method public void refreshVersionsAsync();
- method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer);
+ method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer observer);
+ field public static final androidx.room.InvalidationTracker.Companion Companion;
+ }
+
+ public static final class InvalidationTracker.Companion {
}
public abstract static class InvalidationTracker.Observer {
- ctor protected InvalidationTracker.Observer(String, java.lang.String!...);
- ctor public InvalidationTracker.Observer(String![]);
- method public abstract void onInvalidated(java.util.Set<java.lang.String!>);
+ ctor public InvalidationTracker.Observer(String![] tables);
+ ctor protected InvalidationTracker.Observer(String firstTable, java.lang.String... rest);
+ method public abstract void onInvalidated(java.util.Set<java.lang.String> tables);
}
- @androidx.room.ExperimentalRoomApi public class MultiInstanceInvalidationService extends android.app.Service {
+ @androidx.room.ExperimentalRoomApi public final class MultiInstanceInvalidationService extends android.app.Service {
ctor public MultiInstanceInvalidationService();
- method public android.os.IBinder? onBind(android.content.Intent);
+ method public android.os.IBinder onBind(android.content.Intent intent);
}
public class Room {
ctor @Deprecated public Room();
- method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T!> databaseBuilder(android.content.Context, Class<T!>, String);
- method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T!> inMemoryDatabaseBuilder(android.content.Context, Class<T!>);
+ method public static final <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context context, Class<T> klass, String? name);
+ method public static final <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context context, Class<T> klass);
+ field public static final androidx.room.Room.Companion Companion;
field public static final String MASTER_TABLE_NAME = "room_master_table";
}
+ public static final class Room.Companion {
+ method public <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context context, Class<T> klass, String? name);
+ method public <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context context, Class<T> klass);
+ }
+
public abstract class RoomDatabase {
ctor public RoomDatabase();
method @Deprecated public void beginTransaction();
diff --git a/room/room-runtime/api/restricted_current.ignore b/room/room-runtime/api/restricted_current.ignore
index 757a1db..8bb79fc 100644
--- a/room/room-runtime/api/restricted_current.ignore
+++ b/room/room-runtime/api/restricted_current.ignore
@@ -1,4 +1,12 @@
// Baseline format: 1.0
+ChangedType: androidx.room.InvalidationTracker#createLiveData(String[], boolean, java.util.concurrent.Callable<T>):
+ Method androidx.room.InvalidationTracker.createLiveData has changed return type from androidx.lifecycle.LiveData<T!> to androidx.lifecycle.LiveData<T>
+ChangedType: androidx.room.InvalidationTracker#createLiveData(String[], java.util.concurrent.Callable<T>):
+ Method androidx.room.InvalidationTracker.createLiveData has changed return type from androidx.lifecycle.LiveData<T!> to androidx.lifecycle.LiveData<T>
+ChangedType: androidx.room.Room#databaseBuilder(android.content.Context, Class<T>, String):
+ Method androidx.room.Room.databaseBuilder has changed return type from androidx.room.RoomDatabase.Builder<T!> to androidx.room.RoomDatabase.Builder<T>
+ChangedType: androidx.room.Room#inMemoryDatabaseBuilder(android.content.Context, Class<T>):
+ Method androidx.room.Room.inMemoryDatabaseBuilder has changed return type from androidx.room.RoomDatabase.Builder<T!> to androidx.room.RoomDatabase.Builder<T>
ChangedType: androidx.room.util.FtsTableInfo#columns:
Field androidx.room.util.FtsTableInfo.columns has changed type from java.util.Set<java.lang.String!> to java.util.Set<java.lang.String>
ChangedType: androidx.room.util.FtsTableInfo#options:
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 4900922..0ec9fe0 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -57,30 +57,40 @@
}
public class InvalidationTracker {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.lang.String!...);
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.util.Map<java.lang.String!,java.lang.String!>!, java.util.Map<java.lang.String!,java.util.Set<java.lang.String!>!>!, java.lang.String!...);
- method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void addWeakObserver(androidx.room.InvalidationTracker.Observer!);
- method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T!>! createLiveData(String![]!, java.util.concurrent.Callable<T!>!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T!>! createLiveData(String![]!, boolean, java.util.concurrent.Callable<T!>!);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase database, java.util.Map<java.lang.String,java.lang.String> shadowTablesMap, java.util.Map<java.lang.String,java.util.Set<java.lang.String>> viewTables, java.lang.String... tableNames);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase database, java.lang.String... tableNames);
+ method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer observer);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void addWeakObserver(androidx.room.InvalidationTracker.Observer observer);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T> createLiveData(String![] tableNames, java.util.concurrent.Callable<T> computeFunction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T> createLiveData(String![] tableNames, boolean inTransaction, java.util.concurrent.Callable<T> computeFunction);
method public void refreshVersionsAsync();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void refreshVersionsSync();
- method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer);
+ method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer observer);
+ field public static final androidx.room.InvalidationTracker.Companion Companion;
+ }
+
+ public static final class InvalidationTracker.Companion {
}
public abstract static class InvalidationTracker.Observer {
- ctor protected InvalidationTracker.Observer(String, java.lang.String!...);
- ctor public InvalidationTracker.Observer(String![]);
- method public abstract void onInvalidated(java.util.Set<java.lang.String!>);
+ ctor public InvalidationTracker.Observer(String![] tables);
+ ctor protected InvalidationTracker.Observer(String firstTable, java.lang.String... rest);
+ method public abstract void onInvalidated(java.util.Set<java.lang.String> tables);
}
public class Room {
ctor @Deprecated public Room();
- method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T!> databaseBuilder(android.content.Context, Class<T!>, String);
- method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T!> inMemoryDatabaseBuilder(android.content.Context, Class<T!>);
+ method public static final <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context context, Class<T> klass, String? name);
+ method public static final <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context context, Class<T> klass);
+ field public static final androidx.room.Room.Companion Companion;
field public static final String MASTER_TABLE_NAME = "room_master_table";
}
+ public static final class Room.Companion {
+ method public <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context context, Class<T> klass, String? name);
+ method public <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context context, Class<T> klass);
+ }
+
public abstract class RoomDatabase {
ctor public RoomDatabase();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void assertNotMainThread();
diff --git a/room/room-runtime/src/main/java/androidx/room/AutoCloser.java b/room/room-runtime/src/main/java/androidx/room/AutoCloser.java
index ba157ec..eb67727 100644
--- a/room/room-runtime/src/main/java/androidx/room/AutoCloser.java
+++ b/room/room-runtime/src/main/java/androidx/room/AutoCloser.java
@@ -24,6 +24,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.arch.core.util.Function;
import androidx.room.util.SneakyThrow;
@@ -40,8 +41,12 @@
* SupportSqliteDatabase.
*
* It is important to ensure that the ref count is incremented when using a returned database.
+ *
+ * @hide
*/
-final class AutoCloser {
+// TODO: (b/218894771) Make internal once RoomDatabase is converted to Kotlin.
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public final class AutoCloser {
@Nullable
private SupportSQLiteOpenHelper mDelegateOpenHelper = null;
@@ -305,7 +310,7 @@
*
* @param onAutoClose the callback to run
*/
- public void setAutoCloseCallback(Runnable onAutoClose) {
+ public void setAutoCloseCallback(@NonNull Runnable onAutoClose) {
mOnAutoCloseCallback = onAutoClose;
}
}
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java b/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
deleted file mode 100644
index e1e8155..0000000
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2018 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.VisibleForTesting;
-import androidx.lifecycle.LiveData;
-
-import java.util.Collections;
-import java.util.IdentityHashMap;
-import java.util.Set;
-import java.util.concurrent.Callable;
-
-/**
- * A helper class that maintains {@link RoomTrackingLiveData} instances for an
- * {@link InvalidationTracker}.
- * <p>
- * We keep a strong reference to active LiveData instances to avoid garbage collection in case
- * developer does not hold onto the returned LiveData.
- */
-class InvalidationLiveDataContainer {
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- final Set<LiveData> mLiveDataSet = Collections.newSetFromMap(
- new IdentityHashMap<LiveData, Boolean>()
- );
- private final RoomDatabase mDatabase;
-
- InvalidationLiveDataContainer(RoomDatabase database) {
- mDatabase = database;
- }
-
- <T> LiveData<T> create(String[] tableNames, boolean inTransaction,
- Callable<T> computeFunction) {
- return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
- tableNames);
- }
-
- void onActive(LiveData liveData) {
- mLiveDataSet.add(liveData);
- }
-
- void onInactive(LiveData liveData) {
- mLiveDataSet.remove(liveData);
- }
-}
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.kt
new file mode 100644
index 0000000..2a51d0c
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 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.lifecycle.LiveData
+import java.util.Collections
+import java.util.IdentityHashMap
+import java.util.concurrent.Callable
+
+/**
+ * A helper class that maintains [RoomTrackingLiveData] instances for an
+ * [InvalidationTracker].
+ *
+ * We keep a strong reference to active LiveData instances to avoid garbage collection in case
+ * developer does not hold onto the returned LiveData.
+ */
+internal class InvalidationLiveDataContainer(private val database: RoomDatabase) {
+ internal val liveDataSet: MutableSet<LiveData<*>> = Collections.newSetFromMap(IdentityHashMap())
+
+ fun <T> create(
+ tableNames: Array<String>,
+ inTransaction: Boolean,
+ computeFunction: Callable<T>
+ ): LiveData<T> {
+ return RoomTrackingLiveData(
+ database,
+ this,
+ inTransaction,
+ computeFunction,
+ tableNames
+ )
+ }
+
+ fun onActive(liveData: LiveData<*>) {
+ liveDataSet.add(liveData)
+ }
+
+ fun onInactive(liveData: LiveData<*>) {
+ liveDataSet.remove(liveData)
+ }
+}
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.java b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.java
deleted file mode 100644
index 3a0c5ca..0000000
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.java
+++ /dev/null
@@ -1,900 +0,0 @@
-/*
- * Copyright (C) 2017 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.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.internal.SafeIterableMap;
-import androidx.lifecycle.LiveData;
-import androidx.sqlite.db.SimpleSQLiteQuery;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-import androidx.sqlite.db.SupportSQLiteStatement;
-
-import java.lang.ref.WeakReference;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.Lock;
-
-/**
- * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
- * these tables.
- */
-// Some details on how the InvalidationTracker works:
-// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
-// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
-// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
-// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
-// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
-// tables.
-// * Each update (write operation) on one of the observed tables triggers an update into the
-// memory table table, flipping the invalidated flag ON.
-// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
-// It works as an Observer, and notifies other instances of table invalidation.
-public class InvalidationTracker {
-
- private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
-
- private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
-
- private static final String TABLE_ID_COLUMN_NAME = "table_id";
-
- private static final String INVALIDATED_COLUMN_NAME = "invalidated";
-
- private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
- + "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
- + INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";
-
- @VisibleForTesting
- static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
- + " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
-
- @VisibleForTesting
- static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
- + " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
-
- @NonNull
- final HashMap<String, Integer> mTableIdLookup;
- final String[] mTableNames;
-
- @NonNull
- private Map<String, Set<String>> mViewTables;
-
- @Nullable
- AutoCloser mAutoCloser = null;
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final RoomDatabase mDatabase;
-
- AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
-
- private volatile boolean mInitialized = false;
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- volatile SupportSQLiteStatement mCleanupStatement;
-
- private final ObservedTableTracker mObservedTableTracker;
-
- private final InvalidationLiveDataContainer mInvalidationLiveDataContainer;
-
- // should be accessed with synchronization only.
- @VisibleForTesting
- @SuppressLint("RestrictedApi")
- final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
-
- private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient;
-
- private final Object mSyncTriggersLock = new Object();
-
- /**
- * Used by the generated code.
- *
- * @hide
- */
- @SuppressWarnings("WeakerAccess")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public InvalidationTracker(RoomDatabase database, String... tableNames) {
- this(database, new HashMap<String, String>(), Collections.<String, Set<String>>emptyMap(),
- tableNames);
- }
-
- /**
- * Used by the generated code.
- *
- * @hide
- */
- @SuppressWarnings("WeakerAccess")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public InvalidationTracker(RoomDatabase database, Map<String, String> shadowTablesMap,
- Map<String, Set<String>> viewTables, String... tableNames) {
- mDatabase = database;
- mObservedTableTracker = new ObservedTableTracker(tableNames.length);
- mTableIdLookup = new HashMap<>();
- mViewTables = viewTables;
- mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
- final int size = tableNames.length;
- mTableNames = new String[size];
- for (int id = 0; id < size; id++) {
- final String tableName = tableNames[id].toLowerCase(Locale.US);
- mTableIdLookup.put(tableName, id);
- String shadowTableName = shadowTablesMap.get(tableNames[id]);
- if (shadowTableName != null) {
- mTableNames[id] = shadowTableName.toLowerCase(Locale.US);
- } else {
- mTableNames[id] = tableName;
- }
- }
- // Adjust table id lookup for those tables whose shadow table is another already mapped
- // table (e.g. external content fts tables).
- for (Map.Entry<String, String> shadowTableEntry : shadowTablesMap.entrySet()) {
- String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US);
- if (mTableIdLookup.containsKey(shadowTableName)) {
- String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US);
- mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName));
- }
- }
- }
-
- /**
- * Sets the auto closer for this invalidation tracker so that the invalidation tracker can
- * ensure that the database is not closed if there are pending invalidations that haven't yet
- * been flushed.
- *
- * This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
- * an ok state once the table is invalidated.
- *
- * This must be called before the database is used.
- *
- * @param autoCloser the autocloser associated with the db
- */
- void setAutoCloser(AutoCloser autoCloser) {
- this.mAutoCloser = autoCloser;
- mAutoCloser.setAutoCloseCallback(this::onAutoCloseCallback);
- }
-
- /**
- * Internal method to initialize table tracking.
- * <p>
- * You should never call this method, it is called by the generated code.
- */
- void internalInit(SupportSQLiteDatabase database) {
- synchronized (this) {
- if (mInitialized) {
- Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
- return;
- }
-
- // These actions are not in a transaction because temp_store is not allowed to be
- // performed on a transaction, and recursive_triggers is not affected by transactions.
- database.execSQL("PRAGMA temp_store = MEMORY;");
- database.execSQL("PRAGMA recursive_triggers='ON';");
- database.execSQL(CREATE_TRACKING_TABLE_SQL);
- syncTriggers(database);
- mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
- mInitialized = true;
- }
- }
-
- void onAutoCloseCallback() {
- synchronized (this) {
- mInitialized = false;
- mObservedTableTracker.resetTriggerState();
- }
- }
-
- void startMultiInstanceInvalidation(Context context, String name, Intent serviceIntent) {
- mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name,
- serviceIntent, this, mDatabase.getQueryExecutor());
- }
-
- void stopMultiInstanceInvalidation() {
- if (mMultiInstanceInvalidationClient != null) {
- mMultiInstanceInvalidationClient.stop();
- mMultiInstanceInvalidationClient = null;
- }
- }
-
- private static void appendTriggerName(StringBuilder builder, String tableName,
- String triggerType) {
- builder.append("`")
- .append("room_table_modification_trigger_")
- .append(tableName)
- .append("_")
- .append(triggerType)
- .append("`");
- }
-
- private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
- final String tableName = mTableNames[tableId];
- StringBuilder stringBuilder = new StringBuilder();
- for (String trigger : TRIGGERS) {
- stringBuilder.setLength(0);
- stringBuilder.append("DROP TRIGGER IF EXISTS ");
- appendTriggerName(stringBuilder, tableName, trigger);
- writableDb.execSQL(stringBuilder.toString());
- }
- }
-
- private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
- writableDb.execSQL(
- "INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
- final String tableName = mTableNames[tableId];
- StringBuilder stringBuilder = new StringBuilder();
- for (String trigger : TRIGGERS) {
- stringBuilder.setLength(0);
- stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
- appendTriggerName(stringBuilder, tableName, trigger);
- stringBuilder.append(" AFTER ")
- .append(trigger)
- .append(" ON `")
- .append(tableName)
- .append("` BEGIN UPDATE ")
- .append(UPDATE_TABLE_NAME)
- .append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
- .append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
- .append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
- .append("; END");
- writableDb.execSQL(stringBuilder.toString());
- }
- }
-
- /**
- * Adds the given observer to the observers list and it will be notified if any table it
- * observes changes.
- * <p>
- * Database changes are pulled on another thread so in some race conditions, the observer might
- * be invoked for changes that were done before it is added.
- * <p>
- * If the observer already exists, this is a no-op call.
- * <p>
- * If one of the tables in the Observer does not exist in the database, this method throws an
- * {@link IllegalArgumentException}.
- * <p>
- * This method should be called on a background/worker thread as it performs database
- * operations.
- *
- * @param observer The observer which listens the database for changes.
- */
- @SuppressLint("RestrictedApi")
- @WorkerThread
- public void addObserver(@NonNull Observer observer) {
- final String[] tableNames = resolveViews(observer.mTables);
- int[] tableIds = new int[tableNames.length];
- final int size = tableNames.length;
-
- for (int i = 0; i < size; i++) {
- Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
- if (tableId == null) {
- throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
- }
- tableIds[i] = tableId;
- }
- ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
- ObserverWrapper currentObserver;
- synchronized (mObserverMap) {
- currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
- }
- if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
- syncTriggers();
- }
- }
-
- private String[] validateAndResolveTableNames(String[] tableNames) {
- String[] resolved = resolveViews(tableNames);
- for (String tableName : resolved) {
- if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) {
- throw new IllegalArgumentException("There is no table with name " + tableName);
- }
- }
- return resolved;
- }
-
- /**
- * Resolves the list of tables and views into a list of unique tables that are underlying them.
- *
- * @param names The names of tables or views.
- * @return The names of the underlying tables.
- */
- private String[] resolveViews(String[] names) {
- Set<String> tables = new HashSet<>();
- for (String name : names) {
- final String lowercase = name.toLowerCase(Locale.US);
- if (mViewTables.containsKey(lowercase)) {
- tables.addAll(mViewTables.get(lowercase));
- } else {
- tables.add(name);
- }
- }
- return tables.toArray(new String[tables.size()]);
- }
-
- private static void beginTransactionInternal(SupportSQLiteDatabase database) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
- && database.isWriteAheadLoggingEnabled()) {
- database.beginTransactionNonExclusive();
- } else {
- database.beginTransaction();
- }
- }
-
- /**
- * Adds an observer but keeps a weak reference back to it.
- * <p>
- * Note that you cannot remove this observer once added. It will be automatically removed
- * when the observer is GC'ed.
- *
- * @param observer The observer to which InvalidationTracker will keep a weak reference.
- * @hide
- */
- @SuppressWarnings("unused")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public void addWeakObserver(Observer observer) {
- addObserver(new WeakObserver(this, observer));
- }
-
- /**
- * Removes the observer from the observers list.
- * <p>
- * This method should be called on a background/worker thread as it performs database
- * operations.
- *
- * @param observer The observer to remove.
- */
- @SuppressLint("RestrictedApi")
- @SuppressWarnings("WeakerAccess")
- @WorkerThread
- public void removeObserver(@NonNull final Observer observer) {
- ObserverWrapper wrapper;
- synchronized (mObserverMap) {
- wrapper = mObserverMap.remove(observer);
- }
- if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
- syncTriggers();
- }
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- boolean ensureInitialization() {
- if (!mDatabase.isOpen()) {
- return false;
- }
- if (!mInitialized) {
- // trigger initialization
- mDatabase.getOpenHelper().getWritableDatabase();
- }
- if (!mInitialized) {
- Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
- return false;
- }
- return true;
- }
-
- @VisibleForTesting
- Runnable mRefreshRunnable = new Runnable() {
- @Override
- public void run() {
- final Lock closeLock = mDatabase.getCloseLock();
- Set<Integer> invalidatedTableIds = null;
- closeLock.lock();
- try {
-
- if (!ensureInitialization()) {
- return;
- }
-
- if (!mPendingRefresh.compareAndSet(true, false)) {
- // no pending refresh
- return;
- }
-
- if (mDatabase.inTransaction()) {
- // current thread is in a transaction. when it ends, it will invoke
- // refreshRunnable again. mPendingRefresh is left as false on purpose
- // so that the last transaction can flip it on again.
- return;
- }
-
- // This transaction has to be on the underlying DB rather than the RoomDatabase
- // in order to avoid a recursive loop after endTransaction.
- SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
- db.beginTransactionNonExclusive();
- try {
- invalidatedTableIds = checkUpdatedTable();
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- } catch (IllegalStateException | SQLiteException exception) {
- // may happen if db is closed. just log.
- Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
- exception);
- } finally {
- closeLock.unlock();
-
- if (mAutoCloser != null) {
- mAutoCloser.decrementCountAndScheduleClose();
- }
- }
- if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
- synchronized (mObserverMap) {
- for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
- entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds);
- }
- }
- }
- }
-
- private Set<Integer> checkUpdatedTable() {
- HashSet<Integer> invalidatedTableIds = new HashSet<>();
- Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
- //noinspection TryFinallyCanBeTryWithResources
- try {
- while (cursor.moveToNext()) {
- final int tableId = cursor.getInt(0);
- invalidatedTableIds.add(tableId);
- }
- } finally {
- cursor.close();
- }
- if (!invalidatedTableIds.isEmpty()) {
- mCleanupStatement.executeUpdateDelete();
- }
- return invalidatedTableIds;
- }
- };
-
- /**
- * Enqueues a task to refresh the list of updated tables.
- * <p>
- * This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
- * if you have another connection to the database or directly use {@link
- * SupportSQLiteDatabase}, you may need to call this manually.
- */
- @SuppressWarnings("WeakerAccess")
- public void refreshVersionsAsync() {
- // TODO we should consider doing this sync instead of async.
- if (mPendingRefresh.compareAndSet(false, true)) {
- if (mAutoCloser != null) {
- // refreshVersionsAsync is called with the ref count incremented from
- // RoomDatabase, so the db can't be closed here, but we need to be sure that our
- // db isn't closed until refresh is completed. This increment call must be
- // matched with a corresponding call in mRefreshRunnable.
- mAutoCloser.incrementCountAndEnsureDbIsOpen();
- }
- mDatabase.getQueryExecutor().execute(mRefreshRunnable);
- }
- }
-
- /**
- * Check versions for tables, and run observers synchronously if tables have been updated.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- @WorkerThread
- public void refreshVersionsSync() {
- if (mAutoCloser != null) {
- // This increment call must be matched with a corresponding call in mRefreshRunnable.
- mAutoCloser.incrementCountAndEnsureDbIsOpen();
- }
- syncTriggers();
- mRefreshRunnable.run();
- }
-
- /**
- * Notifies all the registered {@link Observer}s of table changes.
- * <p>
- * This can be used for notifying invalidation that cannot be detected by this
- * {@link InvalidationTracker}, for example, invalidation from another process.
- *
- * @param tables The invalidated tables.
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- public void notifyObserversByTableNames(String... tables) {
- synchronized (mObserverMap) {
- for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
- if (!entry.getKey().isRemote()) {
- entry.getValue().notifyByTableNames(tables);
- }
- }
- }
- }
-
- void syncTriggers(SupportSQLiteDatabase database) {
- if (database.inTransaction()) {
- // we won't run this inside another transaction.
- return;
- }
- try {
- Lock closeLock = mDatabase.getCloseLock();
- closeLock.lock();
- try {
- // Serialize adding and removing table trackers, this is specifically important
- // to avoid missing invalidation before a transaction starts but there are
- // pending (possibly concurrent) observer changes.
- synchronized (mSyncTriggersLock) {
- final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
- if (tablesToSync == null) {
- return;
- }
- final int limit = tablesToSync.length;
- beginTransactionInternal(database);
- try {
- for (int tableId = 0; tableId < limit; tableId++) {
- switch (tablesToSync[tableId]) {
- case ObservedTableTracker.ADD:
- startTrackingTable(database, tableId);
- break;
- case ObservedTableTracker.REMOVE:
- stopTrackingTable(database, tableId);
- break;
- }
- }
- database.setTransactionSuccessful();
- } finally {
- database.endTransaction();
- }
- }
- } finally {
- closeLock.unlock();
- }
- } catch (IllegalStateException | SQLiteException exception) {
- // may happen if db is closed. just log.
- Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
- exception);
- }
- }
-
- /**
- * Called by RoomDatabase before each beginTransaction call.
- * <p>
- * It is important that pending trigger changes are applied to the database before any query
- * runs. Otherwise, we may miss some changes.
- * <p>
- * This api should eventually be public.
- */
- void syncTriggers() {
- if (!mDatabase.isOpen()) {
- return;
- }
- syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
- }
-
- /**
- * Creates a LiveData that computes the given function once and for every other invalidation
- * of the database.
- * <p>
- * Holds a strong reference to the created LiveData as long as it is active.
- *
- * @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
- *
- * @param computeFunction The function that calculates the value
- * @param tableNames The list of tables to observe
- * @param <T> The return type
- * @return A new LiveData that computes the given function when the given list of tables
- * invalidates.
- * @hide
- */
- @Deprecated
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
- return createLiveData(tableNames, false, computeFunction);
- }
-
- /**
- * Creates a LiveData that computes the given function once and for every other invalidation
- * of the database.
- * <p>
- * Holds a strong reference to the created LiveData as long as it is active.
- *
- * @param tableNames The list of tables to observe
- * @param inTransaction True if the computeFunction will be done in a transaction, false
- * otherwise.
- * @param computeFunction The function that calculates the value
- * @param <T> The return type
- * @return A new LiveData that computes the given function when the given list of tables
- * invalidates.
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
- Callable<T> computeFunction) {
- return mInvalidationLiveDataContainer.create(
- validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
- }
-
- /**
- * Wraps an observer and keeps the table information.
- * <p>
- * Internally table ids are used which may change from database to database so the table
- * related information is kept here rather than in the Observer.
- */
- @SuppressWarnings("WeakerAccess")
- static class ObserverWrapper {
- final int[] mTableIds;
- private final String[] mTableNames;
- final Observer mObserver;
- private final Set<String> mSingleTableSet;
-
- ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) {
- mObserver = observer;
- mTableIds = tableIds;
- mTableNames = tableNames;
- if (tableIds.length == 1) {
- HashSet<String> set = new HashSet<>();
- set.add(mTableNames[0]);
- mSingleTableSet = Collections.unmodifiableSet(set);
- } else {
- mSingleTableSet = null;
- }
- }
-
- /**
- * Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated
- * based on the given invalid status set.
- *
- * @param invalidatedTablesIds The table ids of the tables that are invalidated.
- */
- void notifyByTableInvalidStatus(Set<Integer> invalidatedTablesIds) {
- Set<String> invalidatedTables = null;
- final int size = mTableIds.length;
- for (int index = 0; index < size; index++) {
- final int tableId = mTableIds[index];
- if (invalidatedTablesIds.contains(tableId)) {
- if (size == 1) {
- // Optimization for a single-table observer
- invalidatedTables = mSingleTableSet;
- } else {
- if (invalidatedTables == null) {
- invalidatedTables = new HashSet<>(size);
- }
- invalidatedTables.add(mTableNames[index]);
- }
- }
- }
- if (invalidatedTables != null) {
- mObserver.onInvalidated(invalidatedTables);
- }
- }
-
- /**
- * Notifies the underlying {@link #mObserver} if it observes any of the specified
- * {@code tables}.
- *
- * @param tables The invalidated table names.
- */
- void notifyByTableNames(String[] tables) {
- Set<String> invalidatedTables = null;
- if (mTableNames.length == 1) {
- for (String table : tables) {
- if (table.equalsIgnoreCase(mTableNames[0])) {
- // Optimization for a single-table observer
- invalidatedTables = mSingleTableSet;
- break;
- }
- }
- } else {
- HashSet<String> set = new HashSet<>();
- for (String table : tables) {
- for (String ourTable : mTableNames) {
- if (ourTable.equalsIgnoreCase(table)) {
- set.add(ourTable);
- break;
- }
- }
- }
- if (set.size() > 0) {
- invalidatedTables = set;
- }
- }
- if (invalidatedTables != null) {
- mObserver.onInvalidated(invalidatedTables);
- }
- }
- }
-
- /**
- * An observer that can listen for changes in the database.
- */
- public abstract static class Observer {
- final String[] mTables;
-
- /**
- * Observes the given list of tables and views.
- *
- * @param firstTable The name of the table or view.
- * @param rest More names of tables or views.
- */
- @SuppressWarnings("unused")
- protected Observer(@NonNull String firstTable, String... rest) {
- mTables = Arrays.copyOf(rest, rest.length + 1);
- mTables[rest.length] = firstTable;
- }
-
- /**
- * Observes the given list of tables and views.
- *
- * @param tables The list of tables or views to observe for changes.
- */
- public Observer(@NonNull String[] tables) {
- // copy tables in case user modifies them afterwards
- mTables = Arrays.copyOf(tables, tables.length);
- }
-
- /**
- * Called when one of the observed tables is invalidated in the database.
- *
- * @param tables A set of invalidated tables. This is useful when the observer targets
- * multiple tables and you want to know which table is invalidated. This will
- * be names of underlying tables when you are observing views.
- */
- public abstract void onInvalidated(@NonNull Set<String> tables);
-
- boolean isRemote() {
- return false;
- }
- }
-
- /**
- * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
- * triggers in the database.
- * <p>
- * This class is thread safe
- */
- static class ObservedTableTracker {
- static final int NO_OP = 0; // don't change trigger state for this table
- static final int ADD = 1; // add triggers for this table
- static final int REMOVE = 2; // remove triggers for this table
-
- // number of observers per table
- final long[] mTableObservers;
- // trigger state for each table at last sync
- // this field is updated when syncAndGet is called.
- final boolean[] mTriggerStates;
- // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
- final int[] mTriggerStateChanges;
-
- boolean mNeedsSync;
-
- ObservedTableTracker(int tableCount) {
- mTableObservers = new long[tableCount];
- mTriggerStates = new boolean[tableCount];
- mTriggerStateChanges = new int[tableCount];
- Arrays.fill(mTableObservers, 0);
- Arrays.fill(mTriggerStates, false);
- }
-
- /**
- * @return true if # of triggers is affected.
- */
- boolean onAdded(int... tableIds) {
- boolean needTriggerSync = false;
- synchronized (this) {
- for (int tableId : tableIds) {
- final long prevObserverCount = mTableObservers[tableId];
- mTableObservers[tableId] = prevObserverCount + 1;
- if (prevObserverCount == 0) {
- mNeedsSync = true;
- needTriggerSync = true;
- }
- }
- }
- return needTriggerSync;
- }
-
- /**
- * @return true if # of triggers is affected.
- */
- boolean onRemoved(int... tableIds) {
- boolean needTriggerSync = false;
- synchronized (this) {
- for (int tableId : tableIds) {
- final long prevObserverCount = mTableObservers[tableId];
- mTableObservers[tableId] = prevObserverCount - 1;
- if (prevObserverCount == 1) {
- mNeedsSync = true;
- needTriggerSync = true;
- }
- }
- }
- return needTriggerSync;
- }
-
- /**
- * If we are re-opening the db we'll need to add all the triggers that we need so change
- * the current state to false for all.
- */
- void resetTriggerState() {
- synchronized (this) {
- Arrays.fill(mTriggerStates, false);
- mNeedsSync = true;
- }
- }
-
- /**
- * If this returns non-null there are no pending sync operations.
- *
- * @return int[] An int array where the index for each tableId has the action for that
- * table.
- */
- @Nullable
- int[] getTablesToSync() {
- synchronized (this) {
- if (!mNeedsSync) {
- return null;
- }
- final int tableCount = mTableObservers.length;
- for (int i = 0; i < tableCount; i++) {
- final boolean newState = mTableObservers[i] > 0;
- if (newState != mTriggerStates[i]) {
- mTriggerStateChanges[i] = newState ? ADD : REMOVE;
- } else {
- mTriggerStateChanges[i] = NO_OP;
- }
- mTriggerStates[i] = newState;
- }
- mNeedsSync = false;
- return mTriggerStateChanges.clone();
- }
- }
- }
-
- /**
- * An Observer wrapper that keeps a weak reference to the given object.
- * <p>
- * This class will automatically unsubscribe when the wrapped observer goes out of memory.
- */
- static class WeakObserver extends Observer {
- final InvalidationTracker mTracker;
- final WeakReference<Observer> mDelegateRef;
-
- WeakObserver(InvalidationTracker tracker, Observer delegate) {
- super(delegate.mTables);
- mTracker = tracker;
- mDelegateRef = new WeakReference<>(delegate);
- }
-
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- final Observer observer = mDelegateRef.get();
- if (observer == null) {
- mTracker.removeObserver(this);
- } else {
- observer.onInvalidated(tables);
- }
- }
- }
-}
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
new file mode 100644
index 0000000..7352a17
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
@@ -0,0 +1,878 @@
+/*
+ * Copyright (C) 2017 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.Context
+import android.content.Intent
+import android.database.sqlite.SQLiteException
+import android.os.Build
+import android.util.Log
+import androidx.annotation.GuardedBy
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import androidx.arch.core.internal.SafeIterableMap
+import androidx.lifecycle.LiveData
+import androidx.room.Room.Companion.LOG_TAG
+import androidx.room.util.useCursor
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.ref.WeakReference
+import java.util.Arrays
+import java.util.Locale
+import java.util.concurrent.Callable
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
+ * these tables.
+ */
+// Some details on how the InvalidationTracker works:
+// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
+// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
+// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
+// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
+// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
+// tables.
+// * Each update (write operation) on one of the observed tables triggers an update into the
+// memory table table, flipping the invalidated flag ON.
+// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
+// It works as an Observer, and notifies other instances of table invalidation.
+
+open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) constructor(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ @field:RestrictTo(RestrictTo.Scope.LIBRARY)
+ protected val database: RoomDatabase,
+ private val shadowTablesMap: Map<String, String>,
+ private val viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
+ vararg tableNames: String
+) {
+ @JvmField
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ val tableIdLookup: Map<String, Int>
+
+ @JvmField
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ val tablesNames: Array<String>
+
+ private var autoCloser: AutoCloser? = null
+
+ @JvmField
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ val pendingRefresh = AtomicBoolean(false)
+
+ @Volatile
+ private var initialized = false
+
+ @Volatile
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ var cleanupStatement: SupportSQLiteStatement? = null
+
+ private val observedTableTracker: ObservedTableTracker = ObservedTableTracker(tableNames.size)
+
+ private val invalidationLiveDataContainer: InvalidationLiveDataContainer =
+ InvalidationLiveDataContainer(database)
+
+ @GuardedBy("observerMap")
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ protected val observerMap = SafeIterableMap<Observer, ObserverWrapper>()
+
+ private var multiInstanceInvalidationClient: MultiInstanceInvalidationClient? = null
+
+ private val syncTriggersLock = Any()
+
+ private val trackerLock = Any()
+
+ init {
+ tableIdLookup = mutableMapOf()
+ tablesNames = Array(tableNames.size) { id ->
+ val tableName = tableNames[id].lowercase(Locale.US)
+ tableIdLookup[tableName] = id
+ val shadowTableName = shadowTablesMap[tableNames[id]]?.lowercase(Locale.US)
+ shadowTableName ?: tableName
+ }
+
+ // Adjust table id lookup for those tables whose shadow table is another already mapped
+ // table (e.g. external content fts tables).
+ shadowTablesMap.forEach { entry ->
+ val shadowTableName = entry.value.lowercase(Locale.US)
+ if (tableIdLookup.containsKey(shadowTableName)) {
+ val tableName = entry.key.lowercase(Locale.US)
+ tableIdLookup[tableName] = tableIdLookup.getValue(shadowTableName)
+ }
+ }
+ }
+
+ /**
+ * Used by the generated code.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ constructor(database: RoomDatabase, vararg tableNames: String) :
+ this(
+ database = database,
+ shadowTablesMap = emptyMap(),
+ viewTables = emptyMap(),
+ tableNames = tableNames
+ )
+
+ /**
+ * Sets the auto closer for this invalidation tracker so that the invalidation tracker can
+ * ensure that the database is not closed if there are pending invalidations that haven't yet
+ * been flushed.
+ *
+ * This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
+ * an ok state once the table is invalidated.
+ *
+ * This must be called before the database is used.
+ *
+ * @param autoCloser the autocloser associated with the db
+ */
+ // TODO (b/218894771): Make internal when RoomDatabase is converted to Kotlin
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun setAutoCloser(autoCloser: AutoCloser) {
+ this.autoCloser = autoCloser
+ autoCloser.setAutoCloseCallback(::onAutoCloseCallback)
+ }
+
+ /**
+ * Internal method to initialize table tracking.
+ *
+ * You should never call this method, it is called by the generated code.
+ */
+ // TODO (b/218894771): Make internal when RoomDatabase is converted to Kotlin
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun internalInit(database: SupportSQLiteDatabase) {
+ synchronized(trackerLock) {
+ if (initialized) {
+ Log.e(LOG_TAG, "Invalidation tracker is initialized twice :/.")
+ return
+ }
+
+ // These actions are not in a transaction because temp_store is not allowed to be
+ // performed on a transaction, and recursive_triggers is not affected by transactions.
+ database.execSQL("PRAGMA temp_store = MEMORY;")
+ database.execSQL("PRAGMA recursive_triggers='ON';")
+ database.execSQL(CREATE_TRACKING_TABLE_SQL)
+ syncTriggers(database)
+ cleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL)
+ initialized = true
+ }
+ }
+
+ // TODO: Close CleanupStatement
+ internal fun onAutoCloseCallback() {
+ synchronized(trackerLock) {
+ initialized = false
+ observedTableTracker.resetTriggerState()
+ }
+ }
+
+ // TODO (b/218894771): Make internal when RoomDatabase is converted to Kotlin
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun startMultiInstanceInvalidation(context: Context, name: String, serviceIntent: Intent) {
+ multiInstanceInvalidationClient = MultiInstanceInvalidationClient(
+ context = context,
+ name = name,
+ serviceIntent = serviceIntent,
+ invalidationTracker = this,
+ executor = database.queryExecutor
+ )
+ }
+
+ // TODO (b/218894771): Make internal when RoomDatabase is converted to Kotlin
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun stopMultiInstanceInvalidation() {
+ multiInstanceInvalidationClient?.stop()
+ multiInstanceInvalidationClient = null
+ }
+
+ private fun stopTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
+ val tableName = tablesNames[tableId]
+ for (trigger in TRIGGERS) {
+ val sql = buildString {
+ append("DROP TRIGGER IF EXISTS ")
+ append(getTriggerName(tableName, trigger))
+ }
+ db.execSQL(sql)
+ }
+ }
+
+ private fun startTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
+ db.execSQL(
+ "INSERT OR IGNORE INTO $UPDATE_TABLE_NAME VALUES($tableId, 0)"
+ )
+ val tableName = tablesNames[tableId]
+ for (trigger in TRIGGERS) {
+ val sql = buildString {
+ append("CREATE TEMP TRIGGER IF NOT EXISTS ")
+ append(getTriggerName(tableName, trigger))
+ append(" AFTER ")
+ append(trigger)
+ append(" ON `")
+ append(tableName)
+ append("` BEGIN UPDATE ")
+ append(UPDATE_TABLE_NAME)
+ append(" SET ").append(INVALIDATED_COLUMN_NAME)
+ append(" = 1")
+ append(" WHERE ").append(TABLE_ID_COLUMN_NAME)
+ append(" = ").append(tableId)
+ append(" AND ").append(INVALIDATED_COLUMN_NAME)
+ append(" = 0")
+ append("; END")
+ }
+ db.execSQL(sql)
+ }
+ }
+
+ /**
+ * Adds the given observer to the observers list and it will be notified if any table it
+ * observes changes.
+ *
+ * Database changes are pulled on another thread so in some race conditions, the observer might
+ * be invoked for changes that were done before it is added.
+ *
+ * If the observer already exists, this is a no-op call.
+ *
+ * If one of the tables in the Observer does not exist in the database, this method throws an
+ * [IllegalArgumentException].
+ *
+ * This method should be called on a background/worker thread as it performs database
+ * operations.
+ *
+ * @param observer The observer which listens the database for changes.
+ */
+ @SuppressLint("RestrictedApi")
+ @WorkerThread
+ open fun addObserver(observer: Observer) {
+ val tableNames = resolveViews(observer.tables)
+ val tableIds = tableNames.map { tableName ->
+ tableIdLookup[tableName.lowercase(Locale.US)]
+ ?: throw IllegalArgumentException("There is no table with name $tableName")
+ }.toIntArray()
+
+ val wrapper = ObserverWrapper(
+ observer = observer,
+ tableIds = tableIds,
+ tableNames = tableNames
+ )
+
+ val currentObserver = synchronized(observerMap) {
+ observerMap.putIfAbsent(observer, wrapper)
+ }
+ if (currentObserver == null && observedTableTracker.onAdded(*tableIds)) {
+ syncTriggers()
+ }
+ }
+
+ private fun validateAndResolveTableNames(tableNames: Array<String>): Array<String> {
+ val resolved = resolveViews(tableNames)
+ resolved.forEach { tableName ->
+ require(tableIdLookup.containsKey(tableName.lowercase(Locale.US))) {
+ "There is no table with name $tableName"
+ }
+ }
+ return resolved
+ }
+
+ /**
+ * Resolves the list of tables and views into a list of unique tables that are underlying them.
+ *
+ * @param names The names of tables or views.
+ * @return The names of the underlying tables.
+ */
+ private fun resolveViews(names: Array<String>): Array<String> {
+ return buildSet {
+ names.forEach { name ->
+ if (viewTables.containsKey(name.lowercase(Locale.US))) {
+ addAll(viewTables[name.lowercase(Locale.US)]!!)
+ } else {
+ add(name)
+ }
+ }
+ }.toTypedArray()
+ }
+
+ /**
+ * Adds an observer but keeps a weak reference back to it.
+ *
+ * Note that you cannot remove this observer once added. It will be automatically removed
+ * when the observer is GC'ed.
+ *
+ * @param observer The observer to which InvalidationTracker will keep a weak reference.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ open fun addWeakObserver(observer: Observer) {
+ addObserver(WeakObserver(this, observer))
+ }
+
+ /**
+ * Removes the observer from the observers list.
+ *
+ * This method should be called on a background/worker thread as it performs database
+ * operations.
+ *
+ * @param observer The observer to remove.
+ */
+ @SuppressLint("RestrictedApi")
+ @WorkerThread
+ open fun removeObserver(observer: Observer) {
+ val wrapper = synchronized(observerMap) {
+ observerMap.remove(observer)
+ }
+ if (wrapper != null && observedTableTracker.onRemoved(tableIds = wrapper.tableIds)) {
+ syncTriggers()
+ }
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ protected fun ensureInitialization(): Boolean {
+ if (!database.isOpen) {
+ return false
+ }
+ if (!initialized) {
+ // trigger initialization
+ database.openHelper.writableDatabase
+ }
+ if (!initialized) {
+ Log.e(LOG_TAG, "database is not initialized even though it is open")
+ return false
+ }
+ return true
+ }
+
+ @VisibleForTesting
+ @JvmField
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ val refreshRunnable: Runnable = object : Runnable {
+ override fun run() {
+ val closeLock = database.closeLock
+ closeLock.lock()
+ val invalidatedTableIds: Set<Int> =
+ try {
+ if (!ensureInitialization()) {
+ return
+ }
+ if (!pendingRefresh.compareAndSet(true, false)) {
+ // no pending refresh
+ return
+ }
+ if (database.inTransaction()) {
+ // current thread is in a transaction. when it ends, it will invoke
+ // refreshRunnable again. pendingRefresh is left as false on purpose
+ // so that the last transaction can flip it on again.
+ return
+ }
+
+ // This transaction has to be on the underlying DB rather than the RoomDatabase
+ // in order to avoid a recursive loop after endTransaction.
+ val db = database.openHelper.writableDatabase
+ db.beginTransactionNonExclusive()
+ val invalidatedTableIds: Set<Int>
+ try {
+ invalidatedTableIds = checkUpdatedTable()
+ db.setTransactionSuccessful()
+ } finally {
+ db.endTransaction()
+ }
+ invalidatedTableIds
+ } catch (ex: IllegalStateException) {
+ // may happen if db is closed. just log.
+ Log.e(
+ LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
+ ex
+ )
+ emptySet()
+ } catch (ex: SQLiteException) {
+ Log.e(
+ LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
+ ex
+ )
+ emptySet()
+ } finally {
+ closeLock.unlock()
+ autoCloser?.decrementCountAndScheduleClose()
+ }
+
+ if (invalidatedTableIds.isNotEmpty()) {
+ synchronized(observerMap) {
+ observerMap.forEach {
+ it.value.notifyByTableInvalidStatus(invalidatedTableIds)
+ }
+ }
+ }
+ }
+
+ private fun checkUpdatedTable(): Set<Int> {
+ val invalidatedTableIds = buildSet {
+ database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor ->
+ while (cursor.moveToNext()) {
+ add(cursor.getInt(0))
+ }
+ }
+ }
+ if (invalidatedTableIds.isNotEmpty()) {
+ checkNotNull(cleanupStatement)
+ val statement = cleanupStatement
+ requireNotNull(statement)
+ statement.executeUpdateDelete()
+ }
+ return invalidatedTableIds
+ }
+ }
+
+ /**
+ * Enqueues a task to refresh the list of updated tables.
+ *
+ * This method is automatically called when [RoomDatabase.endTransaction] is called but
+ * if you have another connection to the database or directly use [ ], you may need to call this
+ * manually.
+ */
+ open fun refreshVersionsAsync() {
+ // TODO we should consider doing this sync instead of async.
+ if (pendingRefresh.compareAndSet(false, true)) {
+ // refreshVersionsAsync is called with the ref count incremented from
+ // RoomDatabase, so the db can't be closed here, but we need to be sure that our
+ // db isn't closed until refresh is completed. This increment call must be
+ // matched with a corresponding call in refreshRunnable.
+ autoCloser?.incrementCountAndEnsureDbIsOpen()
+ database.queryExecutor.execute(refreshRunnable)
+ }
+ }
+
+ /**
+ * Check versions for tables, and run observers synchronously if tables have been updated.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @WorkerThread
+ open fun refreshVersionsSync() {
+ // This increment call must be matched with a corresponding call in refreshRunnable.
+ autoCloser?.incrementCountAndEnsureDbIsOpen()
+ syncTriggers()
+ refreshRunnable.run()
+ }
+
+ /**
+ * Notifies all the registered [Observer]s of table changes.
+ *
+ * This can be used for notifying invalidation that cannot be detected by this
+ * [InvalidationTracker], for example, invalidation from another process.
+ *
+ * @param tables The invalidated tables.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ fun notifyObserversByTableNames(vararg tables: String) {
+ synchronized(observerMap) {
+ observerMap.forEach { (observer, wrapper) ->
+ if (!observer.isRemote) {
+ wrapper.notifyByTableNames(tables)
+ }
+ }
+ }
+ }
+
+ // TODO (b/218894771): Make internal when RoomDatabase is converted to Kotlin
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ protected fun syncTriggers(database: SupportSQLiteDatabase) {
+ if (database.inTransaction()) {
+ // we won't run this inside another transaction.
+ return
+ }
+ try {
+ val closeLock = this.database.closeLock
+ closeLock.lock()
+ try {
+ // Serialize adding and removing table trackers, this is specifically important
+ // to avoid missing invalidation before a transaction starts but there are
+ // pending (possibly concurrent) observer changes.
+ synchronized(syncTriggersLock) {
+ val tablesToSync = observedTableTracker.getTablesToSync() ?: return
+ beginTransactionInternal(database)
+ try {
+ tablesToSync.forEachIndexed { tableId, syncState ->
+ when (syncState) {
+ ObservedTableTracker.ADD ->
+ startTrackingTable(database, tableId)
+ ObservedTableTracker.REMOVE ->
+ stopTrackingTable(database, tableId)
+ }
+ }
+ database.setTransactionSuccessful()
+ } finally {
+ database.endTransaction()
+ }
+ }
+ } finally {
+ closeLock.unlock()
+ }
+ } catch (ex: IllegalStateException) {
+ // may happen if db is closed. just log.
+ Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
+ } catch (ex: SQLiteException) {
+ Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
+ }
+ }
+
+ /**
+ * Called by RoomDatabase before each beginTransaction call.
+ *
+ * It is important that pending trigger changes are applied to the database before any query
+ * runs. Otherwise, we may miss some changes.
+ *
+ * This api should eventually be public.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun syncTriggers() {
+ if (!database.isOpen) {
+ return
+ }
+ syncTriggers(database.openHelper.writableDatabase)
+ }
+
+ /**
+ * Creates a LiveData that computes the given function once and for every other invalidation
+ * of the database.
+ *
+ * Holds a strong reference to the created LiveData as long as it is active.
+ *
+ * @param computeFunction The function that calculates the value
+ * @param tableNames The list of tables to observe
+ * @param T The return type
+ * @return A new LiveData that computes the given function when the given list of tables
+ * invalidates.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @Deprecated("Use [createLiveData(String[], boolean, Callable)]")
+ open fun <T> createLiveData(
+ tableNames: Array<String>,
+ computeFunction: Callable<T>
+ ): LiveData<T> {
+ return createLiveData(tableNames, false, computeFunction)
+ }
+
+ /**
+ * Creates a LiveData that computes the given function once and for every other invalidation
+ * of the database.
+ *
+ * Holds a strong reference to the created LiveData as long as it is active.
+ *
+ * @param tableNames The list of tables to observe
+ * @param inTransaction True if the computeFunction will be done in a transaction, false
+ * otherwise.
+ * @param computeFunction The function that calculates the value
+ * @param T The return type
+ * @return A new LiveData that computes the given function when the given list of tables
+ * invalidates.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ open fun <T> createLiveData(
+ tableNames: Array<String>,
+ inTransaction: Boolean,
+ computeFunction: Callable<T>
+ ): LiveData<T> {
+ return invalidationLiveDataContainer.create(
+ validateAndResolveTableNames(tableNames), inTransaction, computeFunction
+ )
+ }
+
+ /**
+ * Wraps an observer and keeps the table information.
+ *
+ * Internally table ids are used which may change from database to database so the table
+ * related information is kept here rather than in the Observer.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ protected class ObserverWrapper(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ val observer: Observer,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ val tableIds: IntArray,
+ private val tableNames: Array<String>
+ ) {
+ private val singleTableSet = if (tableNames.isNotEmpty()) {
+ setOf(tableNames[0])
+ } else {
+ emptySet()
+ }
+
+ init {
+ check(tableIds.size == tableNames.size)
+ }
+
+ /**
+ * Notifies the underlying [.mObserver] if any of the observed tables are invalidated
+ * based on the given invalid status set.
+ *
+ * @param invalidatedTablesIds The table ids of the tables that are invalidated.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun notifyByTableInvalidStatus(invalidatedTablesIds: Set<Int?>) {
+ val invalidatedTables = when (tableIds.size) {
+ 0 -> emptySet()
+ 1 -> if (invalidatedTablesIds.contains(tableIds[0])) {
+ singleTableSet // Optimization for a single-table observer
+ } else {
+ emptySet()
+ }
+ else -> buildSet {
+ tableIds.forEachIndexed { idx, tableId ->
+ if (invalidatedTablesIds.contains(tableId)) {
+ add(tableNames[idx])
+ }
+ }
+ }
+ }
+
+ if (invalidatedTables.isNotEmpty()) {
+ observer.onInvalidated(invalidatedTables)
+ }
+ }
+
+ /**
+ * Notifies the underlying [.mObserver] if it observes any of the specified
+ * `tables`.
+ *
+ * @param tables The invalidated table names.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun notifyByTableNames(tables: Array<out String>) {
+ val invalidatedTables = when (tableNames.size) {
+ 0 -> emptySet()
+ 1 -> if (tables.any { it.equals(tableNames[0], ignoreCase = true) }) {
+ singleTableSet // Optimization for a single-table observer
+ } else {
+ emptySet()
+ }
+ else -> buildSet {
+ tables.forEach { table ->
+ tableNames.forEach ourTablesLoop@{ ourTable ->
+ if (ourTable.equals(table, ignoreCase = true)) {
+ add(ourTable)
+ return@ourTablesLoop
+ }
+ }
+ }
+ }
+ }
+ if (invalidatedTables.isNotEmpty()) {
+ observer.onInvalidated(invalidatedTables)
+ }
+ }
+ }
+
+ /**
+ * An observer that can listen for changes in the database.
+ */
+ abstract class Observer(@get:RestrictTo(RestrictTo.Scope.LIBRARY) val tables: Array<String>) {
+ /**
+ * Observes the given list of tables and views.
+ *
+ * @param firstTable The name of the table or view.
+ * @param rest More names of tables or views.
+ */
+ protected constructor(firstTable: String, vararg rest: String) : this(
+ buildList {
+ addAll(rest)
+ add(firstTable)
+ }.toTypedArray()
+ )
+
+ /**
+ * Called when one of the observed tables is invalidated in the database.
+ *
+ * @param tables A set of invalidated tables. This is useful when the observer targets
+ * multiple tables and you want to know which table is invalidated. This will
+ * be names of underlying tables when you are observing views.
+ */
+ abstract fun onInvalidated(tables: Set<String>)
+
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ open val isRemote: Boolean
+ get() = false
+ }
+
+ /**
+ * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
+ * triggers in the database.
+ *
+ * This class is thread safe
+ */
+ internal class ObservedTableTracker(tableCount: Int) {
+ // number of observers per table
+ val tableObservers = LongArray(tableCount)
+
+ // trigger state for each table at last sync
+ // this field is updated when syncAndGet is called.
+ private val triggerStates = BooleanArray(tableCount)
+
+ // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
+ private val triggerStateChanges = IntArray(tableCount)
+
+ var needsSync = false
+
+ /**
+ * @return true if # of triggers is affected.
+ */
+ fun onAdded(vararg tableIds: Int): Boolean {
+ var needTriggerSync = false
+ synchronized(this) {
+ tableIds.forEach { tableId ->
+ val prevObserverCount = tableObservers[tableId]
+ tableObservers[tableId] = prevObserverCount + 1
+ if (prevObserverCount == 0L) {
+ needsSync = true
+ needTriggerSync = true
+ }
+ }
+ }
+ return needTriggerSync
+ }
+
+ /**
+ * @return true if # of triggers is affected.
+ */
+ fun onRemoved(vararg tableIds: Int): Boolean {
+ var needTriggerSync = false
+ synchronized(this) {
+ tableIds.forEach { tableId ->
+ val prevObserverCount = tableObservers[tableId]
+ tableObservers[tableId] = prevObserverCount - 1
+ if (prevObserverCount == 1L) {
+ needsSync = true
+ needTriggerSync = true
+ }
+ }
+ }
+ return needTriggerSync
+ }
+
+ /**
+ * If we are re-opening the db we'll need to add all the triggers that we need so change
+ * the current state to false for all.
+ */
+ fun resetTriggerState() {
+ synchronized(this) {
+ Arrays.fill(triggerStates, false)
+ needsSync = true
+ }
+ }
+
+ /**
+ * If this returns non-null, you must call onSyncCompleted.
+ *
+ * @return int[] An int array where the index for each tableId has the action for that
+ * table.
+ */
+ @VisibleForTesting
+ @JvmName("getTablesToSync")
+ fun getTablesToSync(): IntArray? {
+ synchronized(this) {
+ if (!needsSync) {
+ return null
+ }
+ tableObservers.forEachIndexed { i, observerCount ->
+ val newState = observerCount > 0
+ if (newState != triggerStates[i]) {
+ triggerStateChanges[i] = if (newState) ADD else REMOVE
+ } else {
+ triggerStateChanges[i] = NO_OP
+ }
+ triggerStates[i] = newState
+ }
+ needsSync = false
+ return triggerStateChanges.clone()
+ }
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ companion object {
+ const val NO_OP = 0 // don't change trigger state for this table
+ const val ADD = 1 // add triggers for this table
+ const val REMOVE = 2 // remove triggers for this table
+ }
+ }
+
+ /**
+ * An Observer wrapper that keeps a weak reference to the given object.
+ *
+ * This class will automatically unsubscribe when the wrapped observer goes out of memory.
+ */
+ internal class WeakObserver(
+ val tracker: InvalidationTracker,
+ delegate: Observer
+ ) : Observer(delegate.tables) {
+ val delegateRef: WeakReference<Observer> = WeakReference(delegate)
+ override fun onInvalidated(tables: Set<String>) {
+ val observer = delegateRef.get()
+ if (observer == null) {
+ tracker.removeObserver(this)
+ } else {
+ observer.onInvalidated(tables)
+ }
+ }
+ }
+
+ companion object {
+ private val TRIGGERS = arrayOf("UPDATE", "DELETE", "INSERT")
+ private const val UPDATE_TABLE_NAME = "room_table_modification_log"
+ private const val TABLE_ID_COLUMN_NAME = "table_id"
+ private const val INVALIDATED_COLUMN_NAME = "invalidated"
+ private const val CREATE_TRACKING_TABLE_SQL =
+ "CREATE TEMP TABLE $UPDATE_TABLE_NAME ($TABLE_ID_COLUMN_NAME INTEGER PRIMARY KEY, " +
+ "$INVALIDATED_COLUMN_NAME INTEGER NOT NULL DEFAULT 0)"
+
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ const val RESET_UPDATED_TABLES_SQL =
+ "UPDATE $UPDATE_TABLE_NAME SET $INVALIDATED_COLUMN_NAME = 0 " +
+ "WHERE $INVALIDATED_COLUMN_NAME = 1"
+
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ const val SELECT_UPDATED_TABLES_SQL =
+ "SELECT * FROM $UPDATE_TABLE_NAME WHERE $INVALIDATED_COLUMN_NAME = 1;"
+
+ internal fun getTriggerName(
+ tableName: String,
+ triggerType: String
+ ) = "`room_table_modification_trigger_${tableName}_$triggerType`"
+
+ internal fun beginTransactionInternal(database: SupportSQLiteDatabase) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
+ database.isWriteAheadLoggingEnabled
+ ) {
+ database.beginTransactionNonExclusive()
+ } else {
+ database.beginTransaction()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.java b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.java
deleted file mode 100644
index cd847acc..0000000
--- a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright 2018 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.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Set;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
- * {@link MultiInstanceInvalidationService}.
- */
-class MultiInstanceInvalidationClient {
-
- /**
- * The application context.
- */
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final Context mAppContext;
-
- /**
- * The name of the database file.
- */
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final String mName;
-
- /**
- * The client ID assigned by {@link MultiInstanceInvalidationService}.
- */
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- int mClientId;
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final InvalidationTracker mInvalidationTracker;
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final InvalidationTracker.Observer mObserver;
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- @Nullable
- IMultiInstanceInvalidationService mService;
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final Executor mExecutor;
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final IMultiInstanceInvalidationCallback mCallback =
- new IMultiInstanceInvalidationCallback.Stub() {
- @Override
- public void onInvalidation(final String[] tables) {
- mExecutor.execute(new Runnable() {
- @Override
- public void run() {
- mInvalidationTracker.notifyObserversByTableNames(tables);
- }
- });
- }
- };
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final AtomicBoolean mStopped = new AtomicBoolean(false);
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final ServiceConnection mServiceConnection = new ServiceConnection() {
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
- mExecutor.execute(mSetUpRunnable);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- mExecutor.execute(mRemoveObserverRunnable);
- mService = null;
- }
-
- };
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final Runnable mSetUpRunnable = new Runnable() {
- @Override
- public void run() {
- try {
- final IMultiInstanceInvalidationService service = mService;
- if (service != null) {
- mClientId = service.registerCallback(mCallback, mName);
- mInvalidationTracker.addObserver(mObserver);
- }
- } catch (RemoteException e) {
- Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
- }
- }
- };
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final Runnable mRemoveObserverRunnable = new Runnable() {
- @Override
- public void run() {
- mInvalidationTracker.removeObserver(mObserver);
- }
- };
-
- /**
- * @param context The Context to be used for binding
- * {@link IMultiInstanceInvalidationService}.
- * @param name The name of the database file.
- * @param serviceIntent The {@link Intent} used for binding
- * {@link IMultiInstanceInvalidationService}.
- * @param invalidationTracker The {@link InvalidationTracker}
- * @param executor The background executor.
- */
- MultiInstanceInvalidationClient(Context context, String name, Intent serviceIntent,
- InvalidationTracker invalidationTracker, Executor executor) {
- mAppContext = context.getApplicationContext();
- mName = name;
- mInvalidationTracker = invalidationTracker;
- mExecutor = executor;
- // Use all tables names for observer.
- final Set<String> tableNames = invalidationTracker.mTableIdLookup.keySet();
- mObserver = new InvalidationTracker.Observer(tableNames.toArray(new String[0])) {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- if (mStopped.get()) {
- return;
- }
- try {
- final IMultiInstanceInvalidationService service = mService;
- if (service != null) {
- service.broadcastInvalidation(mClientId, tables.toArray(new String[0]));
- }
- } catch (RemoteException e) {
- Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e);
- }
- }
-
- @Override
- boolean isRemote() {
- return true;
- }
- };
- mAppContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
- }
-
- void stop() {
- if (mStopped.compareAndSet(false, true)) {
- mInvalidationTracker.removeObserver(mObserver);
- try {
- final IMultiInstanceInvalidationService service = mService;
- if (service != null) {
- service.unregisterCallback(mCallback, mClientId);
- }
- } catch (RemoteException e) {
- Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e);
- }
- mAppContext.unbindService(mServiceConnection);
- }
- }
-}
diff --git a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt
new file mode 100644
index 0000000..bd52304
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2018 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.Intent
+import android.content.ServiceConnection
+import android.content.ComponentName
+import android.content.Context
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import androidx.room.Room.Companion.LOG_TAG
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Handles all the communication from [RoomDatabase] and [InvalidationTracker] to
+ * [MultiInstanceInvalidationService].
+ *
+ * @param context The Context to be used for binding
+ * [IMultiInstanceInvalidationService].
+ * @param name The name of the database file.
+ * @param serviceIntent The [Intent] used for binding
+ * [IMultiInstanceInvalidationService].
+ * @param invalidationTracker The [InvalidationTracker]
+ * @param executor The background executor.
+ */
+internal class MultiInstanceInvalidationClient(
+ context: Context,
+ val name: String,
+ serviceIntent: Intent,
+ val invalidationTracker: InvalidationTracker,
+ val executor: Executor
+) {
+ private val appContext = context.applicationContext
+
+ /**
+ * The client ID assigned by [MultiInstanceInvalidationService].
+ */
+ var clientId = 0
+ lateinit var observer: InvalidationTracker.Observer
+ var service: IMultiInstanceInvalidationService? = null
+
+ val callback: IMultiInstanceInvalidationCallback =
+ object : IMultiInstanceInvalidationCallback.Stub() {
+ override fun onInvalidation(tables: Array<String>) {
+ executor.execute { invalidationTracker.notifyObserversByTableNames(*tables) }
+ }
+ }
+
+ val stopped = AtomicBoolean(false)
+
+ val serviceConnection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, service: IBinder) {
+ this@MultiInstanceInvalidationClient.service =
+ IMultiInstanceInvalidationService.Stub.asInterface(service)
+ executor.execute(setUpRunnable)
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ executor.execute(removeObserverRunnable)
+ service = null
+ }
+ }
+
+ val setUpRunnable = Runnable {
+ try {
+ service?.let {
+ clientId = it.registerCallback(callback, name)
+ invalidationTracker.addObserver(observer)
+ }
+ } catch (e: RemoteException) {
+ Log.w(LOG_TAG, "Cannot register multi-instance invalidation callback", e)
+ }
+ }
+
+ val removeObserverRunnable = Runnable { invalidationTracker.removeObserver(observer) }
+
+ init {
+ // Use all tables names for observer.
+ val tableNames: Set<String> = invalidationTracker.tableIdLookup.keys
+ observer = object : InvalidationTracker.Observer(tableNames.toTypedArray()) {
+ override fun onInvalidated(tables: Set<String>) {
+ if (stopped.get()) {
+ return
+ }
+
+ try {
+ service?.broadcastInvalidation(clientId, tables.toTypedArray())
+ } catch (e: RemoteException) {
+ Log.w(LOG_TAG, "Cannot broadcast invalidation", e)
+ }
+ }
+
+ override val isRemote: Boolean
+ get() = true
+ }
+ appContext.bindService(
+ serviceIntent,
+ serviceConnection,
+ Context.BIND_AUTO_CREATE
+ )
+ }
+
+ fun stop() {
+ if (stopped.compareAndSet(false, true)) {
+ invalidationTracker.removeObserver(observer)
+ try {
+ service?.unregisterCallback(callback, clientId)
+ } catch (e: RemoteException) {
+ Log.w(LOG_TAG, "Cannot unregister multi-instance invalidation callback", e)
+ }
+ appContext.unbindService(serviceConnection)
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.java b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.java
deleted file mode 100644
index 0c82f93..0000000
--- a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright 2018 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.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.HashMap;
-
-/**
- * A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
- * This service runs in the main app process. All the instances of {@link InvalidationTracker}
- * (potentially in other processes) has to connect to this service.
- *
- * <p>The intent to launch it can be specified by
- * {@link RoomDatabase.Builder#setMultiInstanceInvalidationServiceIntent}, although the service is
- * defined in the manifest by default so there should be no need to override it in a normal
- * situation.
- */
-@ExperimentalRoomApi
-public class MultiInstanceInvalidationService extends Service {
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- int mMaxClientId = 0;
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final HashMap<Integer, String> mClientNames = new HashMap<>();
-
- // synthetic access
- @SuppressWarnings("WeakerAccess")
- final RemoteCallbackList<IMultiInstanceInvalidationCallback> mCallbackList =
- new RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
- @Override
- public void onCallbackDied(IMultiInstanceInvalidationCallback callback,
- Object cookie) {
- mClientNames.remove((int) cookie);
- }
- };
-
- private final IMultiInstanceInvalidationService.Stub mBinder =
- new IMultiInstanceInvalidationService.Stub() {
-
- // Assigns a client ID to the client.
- @Override
- public int registerCallback(IMultiInstanceInvalidationCallback callback,
- String name) {
- if (name == null) {
- return 0;
- }
- synchronized (mCallbackList) {
- int clientId = ++mMaxClientId;
- // Use the client ID as the RemoteCallbackList cookie.
- if (mCallbackList.register(callback, clientId)) {
- mClientNames.put(clientId, name);
- return clientId;
- } else {
- --mMaxClientId;
- return 0;
- }
- }
- }
-
- // Explicitly removes the client.
- // The client can die without calling this. In that case, mCallbackList
- // .onCallbackDied() can take care of removal.
- @Override
- public void unregisterCallback(IMultiInstanceInvalidationCallback callback,
- int clientId) {
- synchronized (mCallbackList) {
- mCallbackList.unregister(callback);
- mClientNames.remove(clientId);
- }
- }
-
- // Broadcasts table invalidation to other instances of the same database file.
- // The broadcast is not sent to the caller itself.
- @Override
- public void broadcastInvalidation(int clientId, String[] tables) {
- synchronized (mCallbackList) {
- String name = mClientNames.get(clientId);
- if (name == null) {
- Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered");
- return;
- }
- int count = mCallbackList.beginBroadcast();
- try {
- for (int i = 0; i < count; i++) {
- int targetClientId = (int) mCallbackList.getBroadcastCookie(i);
- String targetName = mClientNames.get(targetClientId);
- if (clientId == targetClientId // This is the caller itself.
- || !name.equals(targetName)) { // Not the same file.
- continue;
- }
- try {
- IMultiInstanceInvalidationCallback callback =
- mCallbackList.getBroadcastItem(i);
- callback.onInvalidation(tables);
- } catch (RemoteException e) {
- Log.w(Room.LOG_TAG, "Error invoking a remote callback", e);
- }
- }
- } finally {
- mCallbackList.finishBroadcast();
- }
- }
- }
- };
-
- @Nullable
- @Override
- public IBinder onBind(@NonNull Intent intent) {
- return mBinder;
- }
-}
diff --git a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.kt b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.kt
new file mode 100644
index 0000000..9b8e836
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2018 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.app.Service
+import android.os.RemoteCallbackList
+import android.content.Intent
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import androidx.room.Room.Companion.LOG_TAG
+
+/**
+ * A [Service] for remote invalidation among multiple [InvalidationTracker] instances.
+ * This service runs in the main app process. All the instances of [InvalidationTracker]
+ * (potentially in other processes) has to connect to this service.
+ *
+ * The intent to launch it can be specified by
+ * [RoomDatabase.Builder.setMultiInstanceInvalidationServiceIntent], although the service is
+ * defined in the manifest by default so there should be no need to override it in a normal
+ * situation.
+ */
+@ExperimentalRoomApi
+class MultiInstanceInvalidationService : Service() {
+ internal var maxClientId = 0
+ internal val clientNames = mutableMapOf<Int, String>()
+
+ internal val callbackList: RemoteCallbackList<IMultiInstanceInvalidationCallback> =
+ object : RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
+ override fun onCallbackDied(
+ callback: IMultiInstanceInvalidationCallback,
+ cookie: Any
+ ) {
+ clientNames.remove(cookie as Int)
+ }
+ }
+
+ private val binder: IMultiInstanceInvalidationService.Stub =
+ object : IMultiInstanceInvalidationService.Stub() {
+ // Assigns a client ID to the client.
+ override fun registerCallback(
+ callback: IMultiInstanceInvalidationCallback,
+ name: String?
+ ): Int {
+ if (name == null) {
+ return 0
+ }
+ synchronized(callbackList) {
+ val clientId = ++maxClientId
+ // Use the client ID as the RemoteCallbackList cookie.
+ return if (callbackList.register(callback, clientId)) {
+ clientNames[clientId] = name
+ clientId
+ } else {
+ --maxClientId
+ 0
+ }
+ }
+ }
+
+ // Explicitly removes the client.
+ // The client can die without calling this. In that case, callbackList
+ // .onCallbackDied() can take care of removal.
+ override fun unregisterCallback(
+ callback: IMultiInstanceInvalidationCallback,
+ clientId: Int
+ ) {
+ synchronized(callbackList) {
+ callbackList.unregister(callback)
+ clientNames.remove(clientId)
+ }
+ }
+
+ // Broadcasts table invalidation to other instances of the same database file.
+ // The broadcast is not sent to the caller itself.
+ override fun broadcastInvalidation(clientId: Int, tables: Array<String>) {
+ synchronized(callbackList) {
+ val name = clientNames[clientId]
+ if (name == null) {
+ Log.w(LOG_TAG, "Remote invalidation client ID not registered")
+ return
+ }
+ val count = callbackList.beginBroadcast()
+ try {
+ for (i in 0 until count) {
+ val targetClientId = callbackList.getBroadcastCookie(i) as Int
+ val targetName = clientNames[targetClientId]
+ if (clientId == targetClientId || name != targetName) {
+ // Skip if this is the caller itself or broadcast is for another
+ // database.
+ continue
+ }
+ try {
+ callbackList.getBroadcastItem(i).onInvalidation(tables)
+ } catch (e: RemoteException) {
+ Log.w(LOG_TAG, "Error invoking a remote callback", e)
+ }
+ }
+ } finally {
+ callbackList.finishBroadcast()
+ }
+ }
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ return binder
+ }
+}
\ No newline at end of file
diff --git a/room/room-runtime/src/main/java/androidx/room/Room.java b/room/room-runtime/src/main/java/androidx/room/Room.java
deleted file mode 100644
index d252099..0000000
--- a/room/room-runtime/src/main/java/androidx/room/Room.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2016 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.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-/**
- * Utility class for Room.
- */
-@SuppressWarnings("unused")
-public class Room {
- static final String LOG_TAG = "ROOM";
- /**
- * The master table where room keeps its metadata information.
- */
- public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
- private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
-
- /**
- * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
- * should keep a reference to it and re-use it.
- *
- * @param context The context for the database. This is usually the Application context.
- * @param klass The abstract class which is annotated with {@link Database} and extends
- * {@link RoomDatabase}.
- * @param name The name of the database file.
- * @param <T> The type of the database class.
- * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
- */
- @SuppressWarnings("WeakerAccess")
- @NonNull
- public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder(
- @NonNull Context context, @NonNull Class<T> klass, @NonNull String name) {
- //noinspection ConstantConditions
- if (name == null || name.trim().length() == 0) {
- throw new IllegalArgumentException("Cannot build a database with null or empty name."
- + " If you are trying to create an in memory database, use Room"
- + ".inMemoryDatabaseBuilder");
- }
- return new RoomDatabase.Builder<>(context, klass, name);
- }
-
- /**
- * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
- * database disappears when the process is killed.
- * Once a database is built, you should keep a reference to it and re-use it.
- *
- * @param context The context for the database. This is usually the Application context.
- * @param klass The abstract class which is annotated with {@link Database} and extends
- * {@link RoomDatabase}.
- * @param <T> The type of the database class.
- * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
- */
- @NonNull
- public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder(
- @NonNull Context context, @NonNull Class<T> klass) {
- return new RoomDatabase.Builder<>(context, klass, null);
- }
-
- @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
- @NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static <T, C> T getGeneratedImplementation(@NonNull Class<C> klass,
- @NonNull String suffix) {
- final String fullPackage = klass.getPackage().getName();
- String name = klass.getCanonicalName();
- final String postPackageName = fullPackage.isEmpty()
- ? name
- : name.substring(fullPackage.length() + 1);
- final String implName = postPackageName.replace('.', '_') + suffix;
- //noinspection TryWithIdenticalCatches
- try {
-
- final String fullClassName = fullPackage.isEmpty()
- ? implName
- : fullPackage + "." + implName;
- @SuppressWarnings("unchecked")
- final Class<T> aClass = (Class<T>) Class.forName(
- fullClassName, true, klass.getClassLoader());
- return aClass.newInstance();
- } catch (ClassNotFoundException e) {
- throw new RuntimeException("cannot find implementation for "
- + klass.getCanonicalName() + ". " + implName + " does not exist");
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Cannot access the constructor"
- + klass.getCanonicalName());
- } catch (InstantiationException e) {
- throw new RuntimeException("Failed to create an instance of "
- + klass.getCanonicalName());
- }
- }
-
- /** @deprecated This type should not be instantiated as it contains only static methods. */
- @Deprecated
- @SuppressWarnings("PrivateConstructorForUtilityClass")
- public Room() {
- }
-}
diff --git a/room/room-runtime/src/main/java/androidx/room/Room.kt b/room/room-runtime/src/main/java/androidx/room/Room.kt
new file mode 100644
index 0000000..dd72c02
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/Room.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 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.annotation.RestrictTo
+
+/**
+ * Utility functions for Room.
+ */
+// TODO (b/225972678) remove class and make top-level functions in Room 3.0.
+open class Room {
+ /** This type should not be instantiated as it contains only static methods. */
+ @Deprecated("This type should not be instantiated as it contains only static methods. ")
+ @SuppressWarnings("PrivateConstructorForUtilityClass")
+ constructor()
+
+ companion object {
+ internal const val LOG_TAG = "ROOM"
+
+ /**
+ * The master table where room keeps its metadata information.
+ */
+ const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
+ private const val CURSOR_CONV_SUFFIX = "_CursorConverter"
+
+ @Suppress("UNCHECKED_CAST")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun <T, C> getGeneratedImplementation(
+ klass: Class<C>,
+ suffix: String
+ ): T {
+ val fullPackage = klass.getPackage()!!.name
+ val name: String = klass.canonicalName!!
+ val postPackageName =
+ if (fullPackage.isEmpty()) name else name.substring(fullPackage.length + 1)
+ val implName = postPackageName.replace('.', '_') + suffix
+ return try {
+ val fullClassName = if (fullPackage.isEmpty()) {
+ implName
+ } else {
+ "$fullPackage.$implName"
+ }
+ val aClass = Class.forName(
+ fullClassName, true, klass.classLoader
+ ) as Class<T>
+ aClass.newInstance()
+ } catch (e: ClassNotFoundException) {
+ throw RuntimeException(
+ "Cannot find implementation for ${klass.canonicalName}. $implName does not " +
+ "exist"
+ )
+ } catch (e: IllegalAccessException) {
+ throw RuntimeException(
+ "Cannot access the constructor $klass.canonicalName"
+ )
+ } catch (e: InstantiationException) {
+ throw RuntimeException(
+ "Failed to create an instance of $klass.canonicalName"
+ )
+ }
+ }
+
+ /**
+ * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
+ * database disappears when the process is killed.
+ * Once a database is built, you should keep a reference to it and re-use it.
+ *
+ * @param context The context for the database. This is usually the Application context.
+ * @param klass The abstract class which is annotated with [Database] and extends
+ * [RoomDatabase].
+ * @param T The type of the database class.
+ * @return A `RoomDatabaseBuilder<T>` which you can use to create the database.
+ */
+ @JvmStatic
+ fun <T : RoomDatabase> inMemoryDatabaseBuilder(
+ context: Context,
+ klass: Class<T>
+ ): RoomDatabase.Builder<T> {
+ return RoomDatabase.Builder(context, klass, null)
+ }
+
+ /**
+ * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
+ * should keep a reference to it and re-use it.
+ *
+ * @param context The context for the database. This is usually the Application context.
+ * @param klass The abstract class which is annotated with [Database] and extends
+ * [RoomDatabase].
+ * @param name The name of the database file.
+ * @param T The type of the database class.
+ * @return A `RoomDatabaseBuilder<T>` which you can use to create the database.
+ */
+ @JvmStatic
+ fun <T : RoomDatabase> databaseBuilder(
+ context: Context,
+ klass: Class<T>,
+ name: String?
+ ): RoomDatabase.Builder<T> {
+ require(!name.isNullOrBlank()) {
+ "Cannot build a database with null or empty name." +
+ " If you are trying to create an in memory database, use Room" +
+ ".inMemoryDatabaseBuilder"
+ }
+ return RoomDatabase.Builder(context, klass, name)
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.java b/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
deleted file mode 100644
index 8df1014..0000000
--- a/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright 2018 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 androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.lifecycle.LiveData;
-
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A LiveData implementation that closely works with {@link InvalidationTracker} to implement
- * database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
- * as they are active.
- * <p>
- * We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are
- * observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in
- * memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()},
- * {@link LiveData#onInactive()} because that would mean missing changes in between or doing an
- * extra query on every UI rotation.
- * <p>
- * This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold
- * strongly by the {@link InvalidationTracker} as long as it is active.
- */
-class RoomTrackingLiveData<T> extends LiveData<T> {
- @SuppressWarnings("WeakerAccess")
- final RoomDatabase mDatabase;
-
- @SuppressWarnings("WeakerAccess")
- final boolean mInTransaction;
-
- @SuppressWarnings("WeakerAccess")
- final Callable<T> mComputeFunction;
-
- private final InvalidationLiveDataContainer mContainer;
-
- @SuppressWarnings("WeakerAccess")
- final InvalidationTracker.Observer mObserver;
-
- @SuppressWarnings("WeakerAccess")
- final AtomicBoolean mInvalid = new AtomicBoolean(true);
-
- @SuppressWarnings("WeakerAccess")
- final AtomicBoolean mComputing = new AtomicBoolean(false);
-
- @SuppressWarnings("WeakerAccess")
- final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false);
-
- @SuppressWarnings("WeakerAccess")
- final Runnable mRefreshRunnable = new Runnable() {
- @WorkerThread
- @Override
- public void run() {
- if (mRegisteredObserver.compareAndSet(false, true)) {
- mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
- }
- boolean computed;
- do {
- computed = false;
- // compute can happen only in 1 thread but no reason to lock others.
- if (mComputing.compareAndSet(false, true)) {
- // as long as it is invalid, keep computing.
- try {
- T value = null;
- while (mInvalid.compareAndSet(true, false)) {
- computed = true;
- try {
- value = mComputeFunction.call();
- } catch (Exception e) {
- throw new RuntimeException("Exception while computing database"
- + " live data.", e);
- }
- }
- if (computed) {
- postValue(value);
- }
- } finally {
- // release compute lock
- mComputing.set(false);
- }
- }
- // check invalid after releasing compute lock to avoid the following scenario.
- // Thread A runs compute()
- // Thread A checks invalid, it is false
- // Main thread sets invalid to true
- // Thread B runs, fails to acquire compute lock and skips
- // Thread A releases compute lock
- // We've left invalid in set state. The check below recovers.
- } while (computed && mInvalid.get());
- }
- };
-
- @SuppressWarnings("WeakerAccess")
- final Runnable mInvalidationRunnable = new Runnable() {
- @MainThread
- @Override
- public void run() {
- boolean isActive = hasActiveObservers();
- if (mInvalid.compareAndSet(false, true)) {
- if (isActive) {
- getQueryExecutor().execute(mRefreshRunnable);
- }
- }
- }
- };
- @SuppressLint("RestrictedApi")
- RoomTrackingLiveData(
- RoomDatabase database,
- InvalidationLiveDataContainer container,
- boolean inTransaction,
- Callable<T> computeFunction,
- String[] tableNames) {
- mDatabase = database;
- mInTransaction = inTransaction;
- mComputeFunction = computeFunction;
- mContainer = container;
- mObserver = new InvalidationTracker.Observer(tableNames) {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
- }
- };
- }
-
- @Override
- protected void onActive() {
- super.onActive();
- mContainer.onActive(this);
- getQueryExecutor().execute(mRefreshRunnable);
- }
-
- @Override
- protected void onInactive() {
- super.onInactive();
- mContainer.onInactive(this);
- }
-
- Executor getQueryExecutor() {
- if (mInTransaction) {
- return mDatabase.getTransactionExecutor();
- } else {
- return mDatabase.getQueryExecutor();
- }
- }
-}
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.kt b/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.kt
new file mode 100644
index 0000000..a2785b8
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018 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 androidx.arch.core.executor.ArchTaskExecutor
+import androidx.lifecycle.LiveData
+import java.lang.Exception
+import java.lang.RuntimeException
+import java.util.concurrent.Callable
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * A LiveData implementation that closely works with [InvalidationTracker] to implement
+ * database drive [androidx.lifecycle.LiveData] queries that are strongly hold as long
+ * as they are active.
+ *
+ * We need this extra handling for [androidx.lifecycle.LiveData] because when they are
+ * observed forever, there is no [androidx.lifecycle.Lifecycle] that will keep them in
+ * memory but they should stay. We cannot add-remove observer in [LiveData.onActive],
+ * [LiveData.onInactive] because that would mean missing changes in between or doing an
+ * extra query on every UI rotation.
+ *
+ * This [LiveData] keeps a weak observer to the [InvalidationTracker] but it is hold
+ * strongly by the [InvalidationTracker] as long as it is active.
+ */
+@SuppressLint("RestrictedApi")
+internal class RoomTrackingLiveData<T> (
+ val database: RoomDatabase,
+ private val container: InvalidationLiveDataContainer,
+ val inTransaction: Boolean,
+ val computeFunction: Callable<T>,
+ tableNames: Array<String>
+) : LiveData<T>() {
+ val observer: InvalidationTracker.Observer = object : InvalidationTracker.Observer(tableNames) {
+ override fun onInvalidated(tables: Set<String>) {
+ ArchTaskExecutor.getInstance().executeOnMainThread(invalidationRunnable)
+ }
+ }
+ val invalid = AtomicBoolean(true)
+ val computing = AtomicBoolean(false)
+ val registeredObserver = AtomicBoolean(false)
+ val refreshRunnable = Runnable {
+ if (registeredObserver.compareAndSet(false, true)) {
+ database.invalidationTracker.addWeakObserver(observer)
+ }
+ var computed: Boolean
+ do {
+ computed = false
+ // compute can happen only in 1 thread but no reason to lock others.
+ if (computing.compareAndSet(false, true)) {
+ // as long as it is invalid, keep computing.
+ try {
+ var value: T? = null
+ while (invalid.compareAndSet(true, false)) {
+ computed = true
+ try {
+ value = computeFunction.call()
+ } catch (e: Exception) {
+ throw RuntimeException(
+ "Exception while computing database live data.",
+ e
+ )
+ }
+ }
+ if (computed) {
+ postValue(value)
+ }
+ } finally {
+ // release compute lock
+ computing.set(false)
+ }
+ }
+ // check invalid after releasing compute lock to avoid the following scenario.
+ // Thread A runs compute()
+ // Thread A checks invalid, it is false
+ // Main thread sets invalid to true
+ // Thread B runs, fails to acquire compute lock and skips
+ // Thread A releases compute lock
+ // We've left invalid in set state. The check below recovers.
+ } while (computed && invalid.get())
+ }
+
+ val invalidationRunnable = Runnable {
+ val isActive = hasActiveObservers()
+ if (invalid.compareAndSet(false, true)) {
+ if (isActive) {
+ queryExecutor.execute(refreshRunnable)
+ }
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun onActive() {
+ super.onActive()
+ container.onActive(this as LiveData<Any>)
+ queryExecutor.execute(refreshRunnable)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun onInactive() {
+ super.onInactive()
+ container.onInactive(this as LiveData<Any>)
+ }
+
+ val queryExecutor: Executor
+ get() = if (inTransaction) {
+ database.transactionExecutor
+ } else {
+ database.queryExecutor
+ }
+}
\ No newline at end of file
diff --git a/room/room-runtime/src/test/java/androidx/room/BuilderTest.java b/room/room-runtime/src/test/java/androidx/room/BuilderTest.java
index 63ce98d..9683a39 100644
--- a/room/room-runtime/src/test/java/androidx/room/BuilderTest.java
+++ b/room/room-runtime/src/test/java/androidx/room/BuilderTest.java
@@ -48,18 +48,6 @@
@RunWith(JUnit4.class)
public class BuilderTest {
@Test(expected = IllegalArgumentException.class)
- public void nullContext() {
- //noinspection ConstantConditions
- Room.databaseBuilder(null, RoomDatabase.class, "bla").build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void nullContext2() {
- //noinspection ConstantConditions
- Room.inMemoryDatabaseBuilder(null, RoomDatabase.class).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
public void nullName() {
//noinspection ConstantConditions
Room.databaseBuilder(mock(Context.class), RoomDatabase.class, null).build();
diff --git a/room/room-runtime/src/test/java/androidx/room/BuilderTest_TestDatabase_Impl.java b/room/room-runtime/src/test/java/androidx/room/BuilderTest_TestDatabase_Impl.java
index d796bdc..b95d27d 100644
--- a/room/room-runtime/src/test/java/androidx/room/BuilderTest_TestDatabase_Impl.java
+++ b/room/room-runtime/src/test/java/androidx/room/BuilderTest_TestDatabase_Impl.java
@@ -18,6 +18,8 @@
import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import org.mockito.Mockito;
+
public class BuilderTest_TestDatabase_Impl extends BuilderTest.TestDatabase {
DatabaseConfiguration mConfig;
@Override
@@ -28,12 +30,12 @@
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
- return null;
+ return Mockito.mock(SupportSQLiteOpenHelper.class);
}
@Override
protected InvalidationTracker createInvalidationTracker() {
- return null;
+ return Mockito.mock(InvalidationTracker.class);
}
@Override
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt b/room/room-runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
index 6034b67..550821f 100644
--- a/room/room-runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
+++ b/room/room-runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
@@ -40,9 +40,9 @@
@Test
fun add() {
val liveData = createLiveData()
- assertThat(container.mLiveDataSet, `is`(emptySet()))
+ assertThat(container.liveDataSet, `is`(emptySet()))
container.onActive(liveData)
- assertThat(container.mLiveDataSet, `is`(setOf(liveData)))
+ assertThat(container.liveDataSet, `is`(setOf(liveData)))
}
@Test
@@ -50,7 +50,7 @@
val liveData = createLiveData()
container.onActive(liveData)
container.onActive(liveData)
- assertThat(container.mLiveDataSet, `is`(setOf(liveData)))
+ assertThat(container.liveDataSet, `is`(setOf(liveData)))
}
@Test
@@ -58,7 +58,7 @@
val liveData = createLiveData()
container.onActive(liveData)
container.onInactive(liveData)
- assertThat(container.mLiveDataSet, `is`(emptySet()))
+ assertThat(container.liveDataSet, `is`(emptySet()))
}
@Test
@@ -67,33 +67,33 @@
container.onActive(liveData)
container.onInactive(liveData)
container.onInactive(liveData)
- assertThat(container.mLiveDataSet, `is`(emptySet()))
+ assertThat(container.liveDataSet, `is`(emptySet()))
}
@Test
fun addRemoveMultiple() {
val ld1 = createLiveData()
val ld2 = createLiveData()
- assertThat(container.mLiveDataSet, `is`(emptySet()))
+ assertThat(container.liveDataSet, `is`(emptySet()))
container.onActive(ld1)
container.onActive(ld2)
- assertThat(container.mLiveDataSet, `is`(setOf(ld1, ld2)))
+ assertThat(container.liveDataSet, `is`(setOf(ld1, ld2)))
container.onInactive(ld1)
- assertThat(container.mLiveDataSet, `is`(setOf(ld2)))
+ assertThat(container.liveDataSet, `is`(setOf(ld2)))
container.onInactive(ld1) // intentional
- assertThat(container.mLiveDataSet, `is`(setOf(ld2)))
+ assertThat(container.liveDataSet, `is`(setOf(ld2)))
container.onActive(ld1)
- assertThat(container.mLiveDataSet, `is`(setOf(ld1, ld2)))
+ assertThat(container.liveDataSet, `is`(setOf(ld1, ld2)))
container.onActive(ld1) // intentional
- assertThat(container.mLiveDataSet, `is`(setOf(ld1, ld2)))
+ assertThat(container.liveDataSet, `is`(setOf(ld1, ld2)))
container.onInactive(ld2)
- assertThat(container.mLiveDataSet, `is`(setOf(ld1)))
+ assertThat(container.liveDataSet, `is`(setOf(ld1)))
container.onInactive(ld1)
- assertThat(container.mLiveDataSet, `is`(emptySet()))
+ assertThat(container.liveDataSet, `is`(emptySet()))
container.onActive(ld1)
- assertThat(container.mLiveDataSet, `is`(setOf(ld1)))
+ assertThat(container.liveDataSet, `is`(setOf(ld1)))
container.onActive(ld2)
- assertThat(container.mLiveDataSet, `is`(setOf(ld1, ld2)))
+ assertThat(container.liveDataSet, `is`(setOf(ld1, ld2)))
}
private fun createLiveData(): LiveData<Any> {
@@ -101,7 +101,7 @@
arrayOf("a", "b"),
false,
createComputeFunction<Any>()
- ) as LiveData
+ )
}
private fun <T> createComputeFunction(): Callable<T> {
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.java b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.java
index 07a7c95..b5bd363 100644
--- a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.java
+++ b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.java
@@ -126,22 +126,22 @@
@Test
public void tableIds() {
- assertThat(mTracker.mTableIdLookup.size(), is(5));
- assertThat(mTracker.mTableIdLookup.get("a"), is(0));
- assertThat(mTracker.mTableIdLookup.get("b"), is(1));
- assertThat(mTracker.mTableIdLookup.get("i"), is(2));
- assertThat(mTracker.mTableIdLookup.get("c"), is(3)); // fts
- assertThat(mTracker.mTableIdLookup.get("d"), is(0)); // external content fts
+ assertThat(mTracker.tableIdLookup.size(), is(5));
+ assertThat(mTracker.tableIdLookup.get("a"), is(0));
+ assertThat(mTracker.tableIdLookup.get("b"), is(1));
+ assertThat(mTracker.tableIdLookup.get("i"), is(2));
+ assertThat(mTracker.tableIdLookup.get("c"), is(3)); // fts
+ assertThat(mTracker.tableIdLookup.get("d"), is(0)); // external content fts
}
@Test
public void tableNames() {
- assertThat(mTracker.mTableNames.length, is(5));
- assertThat(mTracker.mTableNames[0], is("a"));
- assertThat(mTracker.mTableNames[1], is("b"));
- assertThat(mTracker.mTableNames[2], is("i"));
- assertThat(mTracker.mTableNames[3], is("c_content")); // fts
- assertThat(mTracker.mTableNames[4], is("a")); // external content fts
+ assertThat(mTracker.tablesNames.length, is(5));
+ assertThat(mTracker.tablesNames[0], is("a"));
+ assertThat(mTracker.tablesNames[1], is("b"));
+ assertThat(mTracker.tablesNames[2], is("i"));
+ assertThat(mTracker.tablesNames[3], is("c_content")); // fts
+ assertThat(mTracker.tablesNames[4], is("a")); // external content fts
}
@Test
@@ -170,11 +170,11 @@
public void addRemoveObserver() throws Exception {
InvalidationTracker.Observer observer = new LatchObserver(1, "a");
mTracker.addObserver(observer);
- assertThat(mTracker.mObserverMap.size(), is(1));
+ assertThat(mTracker.observerMap.size(), is(1));
mTracker.removeObserver(new LatchObserver(1, "a"));
- assertThat(mTracker.mObserverMap.size(), is(1));
+ assertThat(mTracker.observerMap.size(), is(1));
mTracker.removeObserver(observer);
- assertThat(mTracker.mObserverMap.size(), is(0));
+ assertThat(mTracker.observerMap.size(), is(0));
}
private void drainTasks() throws InterruptedException {
@@ -198,12 +198,12 @@
.thenReturn(mock(Cursor.class));
mTracker.refreshVersionsAsync();
mTracker.refreshVersionsAsync();
- verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
+ verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.refreshRunnable);
drainTasks();
reset(mTaskExecutorRule.getTaskExecutor());
mTracker.refreshVersionsAsync();
- verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
+ verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.refreshRunnable);
}
@Test
@@ -268,7 +268,7 @@
doReturn(false).when(mRoomDatabase).isOpen();
doThrow(new IllegalStateException("foo")).when(mOpenHelper).getWritableDatabase();
mTracker.addObserver(new LatchObserver(1, "a", "b"));
- mTracker.mRefreshRunnable.run();
+ mTracker.refreshRunnable.run();
}
@Test
@@ -449,12 +449,12 @@
setInvalidatedTables(3, 1);
mTracker.addObserver(new LatchObserver(1, "a", "b"));
mTracker.syncTriggers();
- mTracker.mRefreshRunnable.run();
+ mTracker.refreshRunnable.run();
doThrow(new SQLiteException("foo")).when(mRoomDatabase).query(
Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL),
any(Object[].class));
- mTracker.mPendingRefresh.set(true);
- mTracker.mRefreshRunnable.run();
+ mTracker.pendingRefresh.set(true);
+ mTracker.refreshRunnable.run();
}
/**
diff --git a/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt b/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt
index 18c179c..6c745b8 100644
--- a/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt
+++ b/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt
@@ -93,7 +93,7 @@
)
val invalidatedTables = CompletableDeferred<List<String>>()
db.invalidationTracker.addObserver(object : InvalidationTracker.Observer("TestEntity") {
- override fun onInvalidated(tables: MutableSet<String>) {
+ override fun onInvalidated(tables: Set<String>) {
invalidatedTables.complete(tables.toList())
}
})