RxDataStore.kt, the RxJava wrapper for the core API.
Future cls: 1) RxDataBuilder 2) RxDataMigration
Test: RxDataStoreTest.java
Relnote: There is now a RxJava Wrapper for the core DataStore API. Future updates will include a Builder and RxDataMigrations.
Bug: 170311106
Change-Id: I3e7e099488035cca906b5f6e6de0a387acb5b964
diff --git a/datastore/datastore-rxjava2/api/current.txt b/datastore/datastore-rxjava2/api/current.txt
new file mode 100644
index 0000000..0e0b96b
--- /dev/null
+++ b/datastore/datastore-rxjava2/api/current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava2 {
+
+ public final class RxDataStore {
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
+ }
+
+}
+
diff --git a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..0e0b96b
--- /dev/null
+++ b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava2 {
+
+ public final class RxDataStore {
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
+ }
+
+}
+
diff --git a/datastore/datastore-rxjava2/api/res-current.txt b/datastore/datastore-rxjava2/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/datastore/datastore-rxjava2/api/res-current.txt
diff --git a/datastore/datastore-rxjava2/api/restricted_current.txt b/datastore/datastore-rxjava2/api/restricted_current.txt
new file mode 100644
index 0000000..0e0b96b
--- /dev/null
+++ b/datastore/datastore-rxjava2/api/restricted_current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava2 {
+
+ public final class RxDataStore {
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
+ }
+
+}
+
diff --git a/datastore/datastore-rxjava2/build.gradle b/datastore/datastore-rxjava2/build.gradle
new file mode 100644
index 0000000..6cf3913
--- /dev/null
+++ b/datastore/datastore-rxjava2/build.gradle
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ buildTypes{
+ debug {
+ multiDexEnabled = true
+ }
+ }
+
+ sourceSets {
+ test.java.srcDirs += 'src/test-common/java'
+ androidTest.java.srcDirs += 'src/test-common/java'
+ }
+}
+
+
+dependencies {
+ api(KOTLIN_STDLIB)
+ api(KOTLIN_COROUTINES_CORE)
+ api("androidx.annotation:annotation:1.1.0")
+ api(RX_JAVA)
+
+ api(project(":datastore:datastore"))
+
+ implementation(KOTLIN_COROUTINES_RX2)
+
+ testImplementation(JUNIT)
+ testImplementation(KOTLIN_COROUTINES_TEST)
+ testImplementation(TRUTH)
+ testImplementation(project(":internal-testutils-truth"))
+
+ androidTestImplementation(project(":datastore:datastore-core"))
+ androidTestImplementation(project(":datastore:datastore"))
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(KOTLIN_COROUTINES_TEST)
+ androidTestImplementation(TRUTH)
+ androidTestImplementation(project(":internal-testutils-truth"))
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+}
+
+androidx {
+ name = "Android DataStore Core RxJava2 Wrappers"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.DATASTORE
+ inceptionYear = "2020"
+ description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
+ legacyDisableKotlinStrictApiMode = true
+}
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
+ }
+}
\ No newline at end of file
diff --git a/datastore/datastore-rxjava2/src/main/AndroidManifest.xml b/datastore/datastore-rxjava2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bf9d5c2
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.datastore.rxjava2">
+
+</manifest>
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStore.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStore.kt
new file mode 100644
index 0000000..6f49f41
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStore.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("RxDataStore")
+
+package androidx.datastore.rxjava2
+
+import androidx.datastore.core.DataStore
+import io.reactivex.Flowable
+import io.reactivex.Single
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.rx2.asFlowable
+import kotlinx.coroutines.rx2.asSingle
+import kotlinx.coroutines.rx2.await
+import io.reactivex.functions.Function
+
+/**
+ * Gets a reactivex.Flowable of the data from DataStore. See [DataStore.data] for more information.
+ *
+ * Provides efficient, cached (when possible) access to the latest durably persisted state.
+ * The flow will always either emit a value or throw an exception encountered when attempting
+ * to read from disk. If an exception is encountered, collecting again will attempt to read the
+ * data again.
+ *
+ * Do not layer a cache on top of this API: it will be be impossible to guarantee consistency.
+ * Instead, use data.first() to access a single snapshot.
+ *
+ * The Flowable will complete with an IOException when an exception is encountered when reading
+ * data.
+ *
+ * @return a flow representing the current state of the data
+ */
+@ExperimentalCoroutinesApi
+public fun <T : Any> DataStore<T>.data(): Flowable<T> {
+ return this.data.asFlowable()
+}
+
+/**
+ * See [DataStore.updateData]
+ *
+ * Updates the data transactionally in an atomic read-modify-write operation. All operations
+ * are serialized, and the transform itself is a async so it can perform heavy work
+ * such as RPCs.
+ *
+ * The Single completes when the data has been persisted durably to disk (after which
+ * [data] will reflect the update). If the transform or write to disk fails, the
+ * transaction is aborted and the returned Single is completed with the error.
+ *
+ * The transform will be run on the scheduler that DataStore was constructed with.
+ *
+ * @return the snapshot returned by the transform
+ * @throws Exception when thrown by the transform function
+ */
+@ExperimentalCoroutinesApi
+public fun <T : Any> DataStore<T>.updateDataAsync(transform: Function<T, Single<T>>): Single<T> {
+ return CoroutineScope(Dispatchers.Unconfined).async {
+ [email protected] {
+ transform.apply(it).await()
+ }
+ }.asSingle(Dispatchers.Unconfined)
+}
diff --git a/datastore/datastore-rxjava2/src/test-common/java/androidx/datastore/rxjava2/TestingSerializer.kt b/datastore/datastore-rxjava2/src/test-common/java/androidx/datastore/rxjava2/TestingSerializer.kt
new file mode 100644
index 0000000..77a9ab6
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/test-common/java/androidx/datastore/rxjava2/TestingSerializer.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+class TestingSerializer(
+ @Volatile var failReadWithCorruptionException: Boolean = false,
+ @Volatile var failingRead: Boolean = false,
+ @Volatile var failingWrite: Boolean = false
+) : Serializer<Byte> {
+ override fun readFrom(input: InputStream): Byte {
+ if (failReadWithCorruptionException) {
+ throw CorruptionException(
+ "CorruptionException",
+ IOException()
+ )
+ }
+
+ if (failingRead) {
+ throw IOException("I was asked to fail on reads")
+ }
+
+ val read = input.read()
+ if (read == -1) {
+ return 0
+ }
+ return read.toByte()
+ }
+
+ override fun writeTo(t: Byte, output: OutputStream) {
+ if (failingWrite) {
+ throw IOException("I was asked to fail on writes")
+ }
+ output.write(t.toInt())
+ }
+
+ override val defaultValue: Byte = 0
+}
\ No newline at end of file
diff --git a/datastore/datastore-rxjava2/src/test/java/androidx/datastore/rxjava2/RxDataStoreTest.java b/datastore/datastore-rxjava2/src/test/java/androidx/datastore/rxjava2/RxDataStoreTest.java
new file mode 100644
index 0000000..992d3648
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/test/java/androidx/datastore/rxjava2/RxDataStoreTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.DataStoreFactory;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Single;
+import io.reactivex.subscribers.TestSubscriber;
+import kotlinx.coroutines.CoroutineScopeKt;
+import kotlinx.coroutines.Dispatchers;
+
+public class RxDataStoreTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static Single<Byte> incrementByte(Byte byteIn) {
+ return Single.just(++byteIn);
+ }
+
+ @Test
+ public void testGetSingleValue() throws Exception {
+ File newFile = tempFolder.newFile();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ new TestingSerializer(),
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ Byte firstByte = RxDataStore.data(byteStore).blockingFirst();
+ assertThat(firstByte).isEqualTo(0);
+
+ Single<Byte> incrementByte = RxDataStore.updateDataAsync(byteStore,
+ RxDataStoreTest::incrementByte);
+
+ assertThat(incrementByte.blockingGet()).isEqualTo(1);
+
+ firstByte = RxDataStore.data(byteStore).blockingFirst();
+ assertThat(firstByte).isEqualTo(1);
+ }
+
+ @Test
+ public void testTake3() throws Exception {
+ File newFile = tempFolder.newFile();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ new TestingSerializer(),
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+ RxDataStore.updateDataAsync(byteStore, RxDataStoreTest::incrementByte);
+ RxDataStore.updateDataAsync(byteStore, RxDataStoreTest::incrementByte);
+
+ testSubscriber.awaitCount(3).assertValues((byte) 0, (byte) 1, (byte) 2);
+ }
+
+
+ @Test
+ public void testReadFailure() throws Exception {
+ File newFile = tempFolder.newFile();
+ TestingSerializer testingSerializer = new TestingSerializer();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ testingSerializer,
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ testingSerializer.setFailingRead(true);
+
+ TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+ assertThat(testSubscriber.awaitTerminalEvent(5, TimeUnit.SECONDS)).isTrue();
+
+ testSubscriber.assertError(IOException.class);
+
+ testingSerializer.setFailingRead(false);
+
+ testSubscriber = RxDataStore.data(byteStore).test();
+ testSubscriber.awaitCount(1).assertValues((byte) 0);
+ }
+
+ @Test
+ public void testWriteFailure() throws Exception {
+ File newFile = tempFolder.newFile();
+ TestingSerializer testingSerializer = new TestingSerializer();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ testingSerializer,
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+ testingSerializer.setFailingWrite(true);
+ Single<Byte> incrementByte = RxDataStore.updateDataAsync(byteStore,
+ RxDataStoreTest::incrementByte);
+
+ incrementByte.cache().test().await().assertError(IOException.class);
+
+ testSubscriber.awaitCount(1).assertNoErrors().assertValues((byte) 0);
+ testingSerializer.setFailingWrite(false);
+
+ Single<Byte> incrementByte2 = RxDataStore.updateDataAsync(byteStore,
+ RxDataStoreTest::incrementByte);
+ assertThat(incrementByte2.blockingGet()).isEqualTo((byte) 1);
+
+ testSubscriber.awaitCount(2).assertValues((byte) 0, (byte) 1);
+ }
+}
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index fd99621..440faef 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -95,6 +95,7 @@
docs(project(":datastore:datastore-preferences-core"))
docs(project(":datastore:datastore-preferences-core:datastore-preferences-proto"))
docs(project(":datastore:datastore-proto"))
+ docs(project(":datastore:datastore-rxjava2"))
docs(project(":documentfile:documentfile"))
docs(project(":drawerlayout:drawerlayout"))
docs(project(":dynamicanimation:dynamicanimation"))
diff --git a/settings.gradle b/settings.gradle
index d21b5db..e33dbcce 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -282,6 +282,7 @@
includeProject(":datastore:datastore-preferences-core:datastore-preferences-proto",
"datastore/datastore-preferences-core/datastore-preferences-proto", [BuildType.MAIN])
includeProject(":datastore:datastore-proto", "datastore/datastore-proto", [BuildType.MAIN])
+includeProject(":datastore:datastore-rxjava2", "datastore/datastore-rxjava2", [BuildType.MAIN])
includeProject(":datastore:datastore-sampleapp", "datastore/datastore-sampleapp", [BuildType.MAIN])
includeProject(":documentfile:documentfile", "documentfile/documentfile", [BuildType.MAIN])
includeProject(":drawerlayout:drawerlayout", "drawerlayout/drawerlayout", [BuildType.MAIN])