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])