Implement ListenableFuturePagedSource

Provides a Java-friendly API leveraging Guava's ListenableFuture
wrapping the suspending PagedSource API

Test: ./gradlew paging:paging-guava:cC
Change-Id: I0add2127d23223bbcc8f1f19ce5b0b52003a1153
diff --git a/paging/guava/api/3.0.0-alpha01.txt b/paging/guava/api/3.0.0-alpha01.txt
new file mode 100644
index 0000000..ccb1c62
--- /dev/null
+++ b/paging/guava/api/3.0.0-alpha01.txt
@@ -0,0 +1,11 @@
+// Signature format: 3.0
+package androidx.paging {
+
+  public abstract class ListenableFuturePagedSource<Key, Value> extends androidx.paging.PagedSource<Key,Value> {
+    ctor public ListenableFuturePagedSource();
+    method public suspend Object load(androidx.paging.PagedSource.LoadParams<Key> p, kotlin.coroutines.Continuation<? super androidx.paging.PagedSource.LoadResult<Key,Value>> $completion);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.paging.PagedSource.LoadResult<Key,Value>> loadFuture(androidx.paging.PagedSource.LoadParams<Key> params);
+  }
+
+}
+
diff --git a/paging/guava/api/api_lint.ignore b/paging/guava/api/api_lint.ignore
new file mode 100644
index 0000000..810fe69
--- /dev/null
+++ b/paging/guava/api/api_lint.ignore
@@ -0,0 +1,7 @@
+// Baseline format: 1.0
+DocumentExceptions: androidx.paging.ListenableFutureKt#await(com.google.common.util.concurrent.ListenableFuture<R>, kotlin.coroutines.Continuation<? super R>):
+    Method ListenableFutureKt.await appears to be throwing java.lang.Throwable; this should be recorded with a @Throws annotation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions
+
+
+MissingNullability: androidx.paging.ListenableFutureKt#await(com.google.common.util.concurrent.ListenableFuture<R>, kotlin.coroutines.Continuation<? super R>):
+    Missing nullability on method `await` return
diff --git a/paging/guava/api/current.txt b/paging/guava/api/current.txt
new file mode 100644
index 0000000..ccb1c62
--- /dev/null
+++ b/paging/guava/api/current.txt
@@ -0,0 +1,11 @@
+// Signature format: 3.0
+package androidx.paging {
+
+  public abstract class ListenableFuturePagedSource<Key, Value> extends androidx.paging.PagedSource<Key,Value> {
+    ctor public ListenableFuturePagedSource();
+    method public suspend Object load(androidx.paging.PagedSource.LoadParams<Key> p, kotlin.coroutines.Continuation<? super androidx.paging.PagedSource.LoadResult<Key,Value>> $completion);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.paging.PagedSource.LoadResult<Key,Value>> loadFuture(androidx.paging.PagedSource.LoadParams<Key> params);
+  }
+
+}
+
diff --git a/paging/guava/api/public_plus_experimental_3.0.0-alpha01.txt b/paging/guava/api/public_plus_experimental_3.0.0-alpha01.txt
new file mode 100644
index 0000000..ccb1c62
--- /dev/null
+++ b/paging/guava/api/public_plus_experimental_3.0.0-alpha01.txt
@@ -0,0 +1,11 @@
+// Signature format: 3.0
+package androidx.paging {
+
+  public abstract class ListenableFuturePagedSource<Key, Value> extends androidx.paging.PagedSource<Key,Value> {
+    ctor public ListenableFuturePagedSource();
+    method public suspend Object load(androidx.paging.PagedSource.LoadParams<Key> p, kotlin.coroutines.Continuation<? super androidx.paging.PagedSource.LoadResult<Key,Value>> $completion);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.paging.PagedSource.LoadResult<Key,Value>> loadFuture(androidx.paging.PagedSource.LoadParams<Key> params);
+  }
+
+}
+
diff --git a/paging/guava/api/public_plus_experimental_current.txt b/paging/guava/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..ccb1c62
--- /dev/null
+++ b/paging/guava/api/public_plus_experimental_current.txt
@@ -0,0 +1,11 @@
+// Signature format: 3.0
+package androidx.paging {
+
+  public abstract class ListenableFuturePagedSource<Key, Value> extends androidx.paging.PagedSource<Key,Value> {
+    ctor public ListenableFuturePagedSource();
+    method public suspend Object load(androidx.paging.PagedSource.LoadParams<Key> p, kotlin.coroutines.Continuation<? super androidx.paging.PagedSource.LoadResult<Key,Value>> $completion);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.paging.PagedSource.LoadResult<Key,Value>> loadFuture(androidx.paging.PagedSource.LoadParams<Key> params);
+  }
+
+}
+
diff --git a/paging/guava/api/res-3.0.0-alpha01.txt b/paging/guava/api/res-3.0.0-alpha01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/paging/guava/api/res-3.0.0-alpha01.txt
diff --git a/paging/guava/api/restricted_3.0.0-alpha01.txt b/paging/guava/api/restricted_3.0.0-alpha01.txt
new file mode 100644
index 0000000..ccb1c62
--- /dev/null
+++ b/paging/guava/api/restricted_3.0.0-alpha01.txt
@@ -0,0 +1,11 @@
+// Signature format: 3.0
+package androidx.paging {
+
+  public abstract class ListenableFuturePagedSource<Key, Value> extends androidx.paging.PagedSource<Key,Value> {
+    ctor public ListenableFuturePagedSource();
+    method public suspend Object load(androidx.paging.PagedSource.LoadParams<Key> p, kotlin.coroutines.Continuation<? super androidx.paging.PagedSource.LoadResult<Key,Value>> $completion);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.paging.PagedSource.LoadResult<Key,Value>> loadFuture(androidx.paging.PagedSource.LoadParams<Key> params);
+  }
+
+}
+
diff --git a/paging/guava/api/restricted_current.txt b/paging/guava/api/restricted_current.txt
new file mode 100644
index 0000000..ccb1c62
--- /dev/null
+++ b/paging/guava/api/restricted_current.txt
@@ -0,0 +1,11 @@
+// Signature format: 3.0
+package androidx.paging {
+
+  public abstract class ListenableFuturePagedSource<Key, Value> extends androidx.paging.PagedSource<Key,Value> {
+    ctor public ListenableFuturePagedSource();
+    method public suspend Object load(androidx.paging.PagedSource.LoadParams<Key> p, kotlin.coroutines.Continuation<? super androidx.paging.PagedSource.LoadResult<Key,Value>> $completion);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.paging.PagedSource.LoadResult<Key,Value>> loadFuture(androidx.paging.PagedSource.LoadParams<Key> params);
+  }
+
+}
+
diff --git a/paging/guava/build.gradle b/paging/guava/build.gradle
new file mode 100644
index 0000000..d5360f9
--- /dev/null
+++ b/paging/guava/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+dependencies {
+    api(project(":paging:paging-common"))
+    api(KOTLIN_STDLIB)
+    api(project(":concurrent:concurrent-futures-ktx"))
+
+    testImplementation project(':internal-testutils-common')
+    testImplementation(JUNIT)
+    testImplementation(KOTLIN_TEST)
+}
+
+androidx {
+    name = "Android Paging Guava"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenVersion = LibraryVersions.PAGING
+    mavenGroup = LibraryGroups.PAGING
+    inceptionYear = "2019"
+    description = "Android Paging Guava"
+    url = AndroidXExtension.ARCHITECTURE_URL
+}
diff --git a/paging/guava/src/main/AndroidManifest.xml b/paging/guava/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ac2ee69
--- /dev/null
+++ b/paging/guava/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (C) 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.paging.guava">
+</manifest>
diff --git a/paging/guava/src/main/java/androidx/paging/ListenableFuturePagedSource.kt b/paging/guava/src/main/java/androidx/paging/ListenableFuturePagedSource.kt
new file mode 100644
index 0000000..0db9e0d
--- /dev/null
+++ b/paging/guava/src/main/java/androidx/paging/ListenableFuturePagedSource.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.paging
+
+import androidx.concurrent.futures.await
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * [ListenableFuture]-based compatibility wrapper around [PagedSource]'s suspending APIs.
+ */
+abstract class ListenableFuturePagedSource<Key : Any, Value : Any> : PagedSource<Key, Value>() {
+    /**
+     * Loading API for [PagedSource].
+     *
+     * Implement this method to trigger your async load (e.g. from database or network).
+     */
+    abstract fun loadFuture(params: LoadParams<Key>): ListenableFuture<LoadResult<Key, Value>>
+
+    override suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value> {
+        return loadFuture(params).await()
+    }
+}
diff --git a/paging/guava/src/test/java/androidx/paging/ListenableFuturePagedSourceTest.kt b/paging/guava/src/test/java/androidx/paging/ListenableFuturePagedSourceTest.kt
new file mode 100644
index 0000000..dc59b05
--- /dev/null
+++ b/paging/guava/src/test/java/androidx/paging/ListenableFuturePagedSourceTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2019 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.paging
+
+import androidx.concurrent.futures.ResolvableFuture
+import androidx.paging.PagedSource.LoadParams
+import androidx.paging.PagedSource.LoadResult.Page
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+@RunWith(JUnit4::class)
+class ListenableFuturePagedSourceTest {
+    private fun loadInternal(params: LoadParams<Int>): Page<Int, Int> {
+        val key = params.key!! // Intentionally fail on null keys
+        require(key >= 0) // Intentionally throw on negative key
+
+        return Page(
+            List(params.loadSize) { it + key },
+            prevKey = key - params.loadSize,
+            nextKey = key + params.loadSize
+        )
+    }
+
+    private val pagedSource = object : PagedSource<Int, Int>() {
+        override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
+            return loadInternal(params)
+        }
+    }
+
+    private val listenableFuturePagedSource = object : ListenableFuturePagedSource<Int, Int>() {
+        override fun loadFuture(params: LoadParams<Int>): ListenableFuture<LoadResult<Int, Int>> {
+            val future = ResolvableFuture.create<LoadResult<Int, Int>>()
+            try {
+                future.set(loadInternal(params))
+            } catch (e: IllegalArgumentException) {
+                future.setException(e)
+            }
+            return future
+        }
+    }
+
+    @Test
+    fun basic() = runBlocking {
+        val params = LoadParams(LoadType.REFRESH, 0, 2, false, 2)
+        assertEquals(pagedSource.load(params), listenableFuturePagedSource.load(params))
+    }
+
+    @Test
+    fun error() {
+        runBlocking {
+            val params = LoadParams<Int>(LoadType.REFRESH, null, 2, false, 2)
+            assertFailsWith<NullPointerException> { pagedSource.load(params) }
+            assertFailsWith<NullPointerException> { listenableFuturePagedSource.load(params) }
+        }
+    }
+
+    @Test
+    fun errorWrapped() {
+        runBlocking {
+            val params = LoadParams(LoadType.REFRESH, -1, 2, false, 2)
+            assertFailsWith<IllegalArgumentException> { pagedSource.load(params) }
+            assertFailsWith<IllegalArgumentException> { listenableFuturePagedSource.load(params) }
+        }
+    }
+}