Fix PageKeyedDataSource + BoundaryCallback - DO NOT MERGE

BoundaryCallback was not firing, because PageKeyedDataSource was
silently swallowing the end of data signal.

Fixes: 111187234
Test: ./gradlew paging:paging-common:test

Change-Id: Iaa9cadda6f631e820ae4ad9910aae3ba9b046607
diff --git a/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
index db6dddf..7c5445d 100644
--- a/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
@@ -337,6 +337,8 @@
         if (key != null) {
             loadAfter(new LoadParams<>(key, pageSize),
                     new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
+        } else {
+            receiver.onPageResult(PageResult.APPEND, PageResult.<Value>getEmptyResult());
         }
     }
 
@@ -348,6 +350,8 @@
         if (key != null) {
             loadBefore(new LoadParams<>(key, pageSize),
                     new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
+        } else {
+            receiver.onPageResult(PageResult.PREPEND, PageResult.<Value>getEmptyResult());
         }
     }
 
diff --git a/paging/common/src/main/java/androidx/paging/PageResult.java b/paging/common/src/main/java/androidx/paging/PageResult.java
index 7893176..e654a73 100644
--- a/paging/common/src/main/java/androidx/paging/PageResult.java
+++ b/paging/common/src/main/java/androidx/paging/PageResult.java
@@ -27,16 +27,29 @@
 import java.util.List;
 
 class PageResult<T> {
+    /**
+     * Single empty instance to avoid allocations.
+     * <p>
+     * Note, distinct from {@link #INVALID_RESULT} because {@link #isInvalid()} checks instance.
+     */
+    @SuppressWarnings("unchecked")
+    private static final PageResult EMPTY_RESULT =
+            new PageResult(Collections.emptyList(), 0);
+
     @SuppressWarnings("unchecked")
     private static final PageResult INVALID_RESULT =
             new PageResult(Collections.emptyList(), 0);
 
     @SuppressWarnings("unchecked")
+    static <T> PageResult<T> getEmptyResult() {
+        return EMPTY_RESULT;
+    }
+
+    @SuppressWarnings("unchecked")
     static <T> PageResult<T> getInvalidResult() {
         return INVALID_RESULT;
     }
 
-
     @Retention(SOURCE)
     @IntDef({INIT, APPEND, PREPEND, TILE})
     @interface ResultType {}
diff --git a/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
index 9f211fc..2010a15 100644
--- a/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
@@ -25,6 +25,7 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
 
 @RunWith(JUnit4::class)
 class PageKeyedDataSourceTest {
@@ -41,8 +42,9 @@
         private fun getPage(key: String): Page = data[key]!!
 
         override fun loadInitial(
-                params: LoadInitialParams<String>,
-                callback: LoadInitialCallback<String, Item>) {
+            params: LoadInitialParams<String>,
+            callback: LoadInitialCallback<String, Item>
+        ) {
             val page = getPage(INIT_KEY)
             callback.onResult(page.data, page.prev, page.next)
         }
@@ -80,13 +82,16 @@
         assertEquals(ITEM_LIST, pagedList)
     }
 
-    private fun performLoadInitial(invalidateDataSource: Boolean = false,
-            callbackInvoker:
-                    (callback: PageKeyedDataSource.LoadInitialCallback<String, String>) -> Unit) {
+    private fun performLoadInitial(
+        invalidateDataSource: Boolean = false,
+        callbackInvoker:
+                (callback: PageKeyedDataSource.LoadInitialCallback<String, String>) -> Unit
+    ) {
         val dataSource = object : PageKeyedDataSource<String, String>() {
             override fun loadInitial(
-                    params: LoadInitialParams<String>,
-                    callback: LoadInitialCallback<String, String>) {
+                params: LoadInitialParams<String>,
+                callback: LoadInitialCallback<String, String>
+            ) {
                 if (invalidateDataSource) {
                     // invalidate data source so it's invalid when onResult() called
                     invalidate()
@@ -95,14 +100,16 @@
             }
 
             override fun loadBefore(
-                    params: LoadParams<String>,
-                    callback: LoadCallback<String, String>) {
+                params: LoadParams<String>,
+                callback: LoadCallback<String, String>
+            ) {
                 fail("loadBefore not expected")
             }
 
             override fun loadAfter(
-                    params: LoadParams<String>,
-                    callback: LoadCallback<String, String>) {
+                params: LoadParams<String>,
+                callback: LoadCallback<String, String>
+            ) {
                 fail("loadAfter not expected")
             }
         }
@@ -159,6 +166,111 @@
         it.onResult(emptyList(), 0, 1, null, null)
     }
 
+    @Test
+    fun testBoundaryCallback() {
+        val dataSource = object : PageKeyedDataSource<String, String>() {
+            override fun loadInitial(
+                params: LoadInitialParams<String>,
+                callback: LoadInitialCallback<String, String>
+            ) {
+                callback.onResult(listOf("B"), "a", "c")
+            }
+
+            override fun loadBefore(
+                params: LoadParams<String>,
+                callback: LoadCallback<String, String>
+            ) {
+                assertEquals("a", params.key)
+                callback.onResult(listOf("A"), null)
+            }
+
+            override fun loadAfter(
+                params: LoadParams<String>,
+                callback: LoadCallback<String, String>
+            ) {
+                assertEquals("c", params.key)
+                callback.onResult(listOf("C"), null)
+            }
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<String>
+        val executor = TestExecutor()
+        val pagedList = ContiguousPagedList<String, String>(
+                dataSource,
+                executor,
+                executor,
+                boundaryCallback,
+                PagedList.Config.Builder()
+                        .setPageSize(10)
+                        .build(),
+                "",
+                ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+        pagedList.loadAround(0)
+
+        verifyZeroInteractions(boundaryCallback)
+
+        executor.executeAll()
+
+        // verify boundary callbacks are triggered
+        verify(boundaryCallback).onItemAtFrontLoaded("A")
+        verify(boundaryCallback).onItemAtEndLoaded("C")
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
+    @Test
+    fun testBoundaryCallbackJustInitial() {
+        val dataSource = object : PageKeyedDataSource<String, String>() {
+            override fun loadInitial(
+                params: LoadInitialParams<String>,
+                callback: LoadInitialCallback<String, String>
+            ) {
+                // just the one load, but boundary callbacks should still be triggered
+                callback.onResult(listOf("B"), null, null)
+            }
+
+            override fun loadBefore(
+                params: LoadParams<String>,
+                callback: LoadCallback<String, String>
+            ) {
+                fail("loadBefore not expected")
+            }
+
+            override fun loadAfter(
+                params: LoadParams<String>,
+                callback: LoadCallback<String, String>
+            ) {
+                fail("loadBefore not expected")
+            }
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<String>
+        val executor = TestExecutor()
+        val pagedList = ContiguousPagedList<String, String>(
+                dataSource,
+                executor,
+                executor,
+                boundaryCallback,
+                PagedList.Config.Builder()
+                        .setPageSize(10)
+                        .build(),
+                "",
+                ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+        pagedList.loadAround(0)
+
+        verifyZeroInteractions(boundaryCallback)
+
+        executor.executeAll()
+
+        // verify boundary callbacks are triggered
+        verify(boundaryCallback).onItemAtFrontLoaded("B")
+        verify(boundaryCallback).onItemAtEndLoaded("B")
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
     private abstract class WrapperDataSource<K, A, B>(private val source: PageKeyedDataSource<K, A>)
             : PageKeyedDataSource<K, B>() {
         override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
@@ -177,11 +289,18 @@
             return source.isInvalid
         }
 
-        override fun loadInitial(params: LoadInitialParams<K>,
-                callback: LoadInitialCallback<K, B>) {
+        override fun loadInitial(
+            params: LoadInitialParams<K>,
+            callback: LoadInitialCallback<K, B>
+        ) {
             source.loadInitial(params, object : LoadInitialCallback<K, A>() {
-                override fun onResult(data: List<A>, position: Int, totalCount: Int,
-                        previousPageKey: K?, nextPageKey: K?) {
+                override fun onResult(
+                    data: List<A>,
+                    position: Int,
+                    totalCount: Int,
+                    previousPageKey: K?,
+                    nextPageKey: K?
+                ) {
                     callback.onResult(convert(data), position, totalCount,
                             previousPageKey, nextPageKey)
                 }
@@ -218,8 +337,9 @@
         }
     }
 
-    private fun verifyWrappedDataSource(createWrapper:
-            (PageKeyedDataSource<String, Item>) -> PageKeyedDataSource<String, String>) {
+    private fun verifyWrappedDataSource(
+        createWrapper: (PageKeyedDataSource<String, Item>) -> PageKeyedDataSource<String, String>
+    ) {
         // verify that it's possible to wrap a PageKeyedDataSource, and add info to its data
         val orig = ItemDataSource(data = PAGE_MAP)
         val wrapper = createWrapper(orig)