Improve docs, add PosDataSource.InitialLoadCallback#onResult(list, int)

Clarify several things in docs:
- Positional initial load when placeholders disabled
- Positional loadRange requires page sizes respected, others don't
- BoundaryCallback network + database usage

Test: Tests in paging-common
Test: ./gradlew createFlatfootDocsArchive -PofflineDocs=true

Change-Id: I8a3f5b286d349c71d1c35882a14098302a75498d
diff --git a/paging/common/src/main/java/android/arch/paging/DataSource.java b/paging/common/src/main/java/android/arch/paging/DataSource.java
index 635a910..23f8154 100644
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/DataSource.java
@@ -69,16 +69,19 @@
  * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
  * <p>
  * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
- * example a network response that returns some items, and a next page and previous page links.
+ * example a network response that returns some items, and a next/previous page links.
  * <p>
  * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
  * {@code N}. For example, if requesting the backend for the next comments in the list
  * requires the ID or timestamp of the most recent loaded comment, or if querying the next users
  * from a name-sorted database query requires the name and unique ID of the previous.
  * <p>
- * Use {@link PositionalDataSource} if you can load arbitrary pages based solely on position
- * information, and can provide a fixed item count. PositionalDataSource supports querying pages at
- * arbitrary positions, so can provide data to PagedLists in arbitrary order.
+ * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
+ * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
+ * PositionalDataSource is required to respect page size for efficient tiling. If you want to
+ * override page size (e.g. when network page size constraints are only known at runtime), use one
+ * of the other DataSource classes.
  * <p>
  * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
  * return {@code null} items in lists that it loads. This is so that users of the PagedList
@@ -114,8 +117,13 @@
         /**
          * Create a DataSource.
          * <p>
-         * The DataSource should invalidate itself if the snapshot is no longer valid, and a new
-         * DataSource should be queried from the Factory.
+         * The DataSource should invalidate itself if the snapshot is no longer valid. If a
+         * DataSource becomes invalid, the only way to query more data is to create a new DataSource
+         * from the Factory.
+         * <p>
+         * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
+         * when the current DataSource is invalidated, and pass the new PagedList through the
+         * {@code LiveData<PagedList>} to observers.
          *
          * @return the new DataSource.
          */
diff --git a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
index 83278f8..5fb999b 100644
--- a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
@@ -108,6 +108,12 @@
      * <p>
      * A callback can be called only once, and will throw if called again.
      * <p>
+     * If you can compute the number of items in the data set before and after the loaded range,
+     * call the three parameter {@link #onResult(List, int, int)} to pass that information. You
+     * can skip passing this information by calling the single parameter {@link #onResult(List)},
+     * either if it's difficult to compute, or if {@link LoadInitialParams#placeholdersEnabled} is
+     * {@code false}, so the positioning information will be ignored.
+     * <p>
      * It is always valid for a DataSource loading method that takes a callback to stash the
      * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
      * temporary, recoverable error states (such as a network error that can be retried).
diff --git a/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
index f406dee..5df12e5 100644
--- a/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
@@ -139,6 +139,13 @@
      * <p>
      * A callback can be called only once, and will throw if called again.
      * <p>
+     * If you can compute the number of items in the data set before and after the loaded range,
+     * call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that
+     * information. You can skip passing this information by calling the three parameter
+     * {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if
+     * {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning
+     * information will be ignored.
+     * <p>
      * It is always valid for a DataSource loading method that takes a callback to stash the
      * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
      * temporary, recoverable error states (such as a network error that can be retried).
diff --git a/paging/common/src/main/java/android/arch/paging/PagedList.java b/paging/common/src/main/java/android/arch/paging/PagedList.java
index b6a95df..4bc9aa0 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedList.java
@@ -908,13 +908,26 @@
     /**
      * Signals when a PagedList has reached the end of available data.
      * <p>
-     * This can be used to implement paging from the network into a local database - when the
-     * database has no more data to present, a BoundaryCallback can be used to fetch more data.
+     * When local storage is a cache of network data, it's common to set up a streaming pipeline:
+     * Network data is paged into the database, database is paged into UI. Paging from the database
+     * to UI can be done with a {@code LiveData<PagedList>}, but it's still necessary to know when
+     * to trigger network loads.
      * <p>
-     * If an instance is shared across multiple PagedLists (e.g. when passed to
+     * BoundaryCallback does this signaling - when a DataSource runs out of data at the end of
+     * the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network
+     * load that will write the result directly to the database. Because the database is being
+     * observed, the UI bound to the {@code LiveData<PagedList>} will update automatically to
+     * account for the new items.
+     * <p>
+     * Note that a BoundaryCallback instance shared across multiple PagedLists (e.g. when passed to
      * {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple
      * times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should
      * avoid triggering it again while the load is ongoing.
+     * <p>
+     * BoundaryCallback only passes the item at front or end of the list. Number of items is not
+     * passed, since it may not be fully computed by the DataSource if placeholders are not
+     * supplied. Keys are not known because the BoundaryCallback is independent of the
+     * DataSource-specific keys, which may be different for local vs remote storage.
      *
      * @param <T> Type loaded by the PagedList.
      */
diff --git a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
index 819d56f..4b9f1c0 100644
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
@@ -25,15 +25,19 @@
 import java.util.concurrent.Executor;
 
 /**
- * Position-based data loader for a fixed-size, countable data set, supporting loads at arbitrary
- * positions.
+ * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
+ * arbitrary page positions.
  * <p>
- * Extend PositionalDataSource if you can support counting your data set, and loading based on
- * position information.
+ * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. If your data source can't support loading arbitrary
+ * requested page sizes (e.g. when network page size constraints are only known at runtime), use
+ * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
  * <p>
  * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
- * PositionalDataSource requires counting the size of the dataset. This allows pages to be tiled in
+ * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
  * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
+ * If placeholders are disabled, initialize with the two parameter
+ * {@link LoadInitialCallback#onResult(List, int)}.
  * <p>
  * Room can generate a Factory of PositionalDataSources for you:
  * <pre>
@@ -148,7 +152,9 @@
          * Call this method from your DataSource's {@code loadInitial} function to return data,
          * and inform how many placeholders should be shown before and after. If counting is cheap
          * to compute (for example, if a network load returns the information regardless), it's
-         * recommended to pass data back through this method.
+         * recommended to pass the total size to the totalCount parameter. If placeholders are not
+         * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
+         * call {@link #onResult(List, int)}.
          *
          * @param data List of items loaded from the DataSource. If this is empty, the DataSource
          *             is treated as empty, and no further loads will occur.
@@ -179,11 +185,14 @@
         }
 
         /**
-         * Called to pass initial load state from a DataSource without supporting placeholders.
+         * Called to pass initial load state from a DataSource without total count,
+         * when placeholders aren't requested.
+         * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
+         * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
          * <p>
          * Call this method from your DataSource's {@code loadInitial} function to return data,
-         * if position is known but total size is not. If counting is not expensive, consider
-         * calling the three parameter variant: {@link #onResult(List, int, int)}.
+         * if position is known but total size is not. If placeholders are requested, call the three
+         * parameter variant: {@link #onResult(List, int, int)}.
          *
          * @param data List of items loaded from the DataSource. If this is empty, the DataSource
          *             is treated as empty, and no further loads will occur.
@@ -191,10 +200,21 @@
          *                 items before the items in data that can be provided by this DataSource,
          *                 pass {@code N}.
          */
-        void onResult(@NonNull List<T> data, int position) {
-            // not counting, don't need to check mAcceptCount
-            dispatchResultToReceiver(new PageResult<>(
-                    data, 0, 0, position));
+        @SuppressWarnings("WeakerAccess")
+        public void onResult(@NonNull List<T> data, int position) {
+            if (position < 0) {
+                throw new IllegalArgumentException("Position must be non-negative");
+            }
+            if (data.isEmpty() && position != 0) {
+                throw new IllegalArgumentException(
+                        "Initial result cannot be empty if items are present in data set.");
+            }
+            if (mCountingEnabled) {
+                throw new IllegalStateException("Placeholders requested, but totalCount not"
+                        + " provided. Please call the three-parameter onResult method, or disable"
+                        + " placeholders in the PagedList.Config");
+            }
+            dispatchResultToReceiver(new PageResult<>(data, position));
         }
     }
 
@@ -237,7 +257,7 @@
                 new LoadInitialCallback<>(this, acceptCount, pageSize, receiver);
 
         LoadInitialParams params = new LoadInitialParams(
-                requestedStartPosition, requestedLoadSize, pageSize, true);
+                requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
         loadInitial(params, callback);
 
         // If initialLoad's callback is not called within the body, we force any following calls
diff --git a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
index da4b265..280a64d 100644
--- a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
@@ -80,6 +80,7 @@
     }
 
     private fun performInitialLoad(
+            enablePlaceholders: Boolean = true,
             callbackInvoker: (callback: PositionalDataSource.LoadInitialCallback<String>) -> Unit) {
         val dataSource = object : PositionalDataSource<String>() {
             override fun loadInitial(
@@ -93,12 +94,16 @@
             }
         }
 
-        TiledPagedList(
-                dataSource, FailExecutor(), FailExecutor(), null,
-                PagedList.Config.Builder()
-                        .setPageSize(10)
-                        .build(),
-                0)
+        val config = PagedList.Config.Builder()
+                .setPageSize(10)
+                .setEnablePlaceholders(enablePlaceholders)
+                .build()
+        if (enablePlaceholders) {
+            TiledPagedList(dataSource, FailExecutor(), FailExecutor(), null, config, 0)
+        } else {
+            ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
+                    FailExecutor(), FailExecutor(), null, config, null)
+        }
     }
 
     @Test
@@ -137,4 +142,28 @@
         // LoadInitialCallback can't accept empty result unless data set is empty
         it.onResult(emptyList(), 0, 2)
     }
+
+    @Test(expected = IllegalStateException::class)
+    fun initialLoadCallbackRequireTotalCount() = performInitialLoad(enablePlaceholders = true) {
+        // LoadInitialCallback requires 3 args when placeholders enabled
+        it.onResult(listOf("a", "b"), 0)
+    }
+
+    @Test
+    fun initialLoadCallbackSuccessTwoArg() = performInitialLoad(enablePlaceholders = false) {
+        // LoadInitialCallback correct 2 arg usage
+        it.onResult(listOf("a", "b"), 0)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialLoadCallbackPosNegativeTwoArg() = performInitialLoad(enablePlaceholders = false) {
+        // LoadInitialCallback can't accept negative position
+        it.onResult(listOf("a", "b"), -1)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialLoadCallbackEmptyWithOffset() = performInitialLoad(enablePlaceholders = false) {
+        // LoadInitialCallback can't accept empty result unless pos is 0
+        it.onResult(emptyList(), 1)
+    }
 }