Merge "Fix IndexOutOfBoundException in position lookup" into androidx-master-dev
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt
index e9b0eac..afddc32 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt
@@ -205,6 +205,28 @@
         }
     }
 
+    @Test
+    fun transformAnchorIndex_loadingSnapshot() {
+        val oldList = PagedStorage(10, listOf("a"), 10)
+        val newList = PagedStorage(10, listOf("a"), 10)
+
+        oldList.allocatePlaceholders(10, 5, 1,
+                /* ignored */ mock(PagedStorage.Callback::class.java))
+
+        assertEquals(5, oldList.leadingNullCount)
+        assertEquals(10, oldList.computeLeadingNulls())
+
+        validateTwoListDiffTransform(
+                oldList,
+                newList) { transformAnchorIndex ->
+            // previously, this would cause a crash where we tried to use storage space
+            // (getLeadingNullCount..size-getTrailingNulls) incorrectly, instead of diff space
+            // (computeLeadingNulls..size-computeTrailingNulls). Diff space greedily excludes the
+            // nulls that represent partially loaded pages to minimize diff computation cost.
+            assertEquals(15, transformAnchorIndex(15))
+        }
+    }
+
     companion object {
         private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
             override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
diff --git a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
index b3fa608..27e6c01 100644
--- a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
+++ b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
@@ -374,9 +374,12 @@
 
         // Transform the last loadAround() index from the old list to the new list by passing it
         // through the DiffResult. This ensures the lastKey of a positional PagedList is carried
-        // to new list even if no in-viewport item changes (AsyncPagedListDiffer#get not called)
+        // to new list even if no in-viewport item changes (AsyncPagedListDiffer#get not called).
+        // Note: we don't take into account loads between new list snapshot and new list, but this
+        // is only a problem in rare cases when placeholders are disabled, and a load starts (for
+        // some reason) and finishes before diff completes.
         int newPosition = PagedStorageDiffHelper.transformAnchorIndex(
-                diffResult, previousSnapshot.mStorage, newList.mStorage, lastAccessIndex);
+                diffResult, previousSnapshot.mStorage, diffSnapshot.mStorage, lastAccessIndex);
         // copy lastLoad position, clamped to list bounds
         mPagedList.mLastLoad = Math.max(0, Math.min(mPagedList.size(), newPosition));
 
diff --git a/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.java b/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.java
index e98cdd1..a89baf9 100644
--- a/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.java
+++ b/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.java
@@ -21,6 +21,21 @@
 import androidx.recyclerview.widget.DiffUtil;
 import androidx.recyclerview.widget.ListUpdateCallback;
 
+/**
+ * Methods for computing and applying DiffResults between PagedLists.
+ *
+ * To minimize the amount of diffing caused by placeholders, we only execute DiffUtil in a reduced
+ * 'diff space' - in the range (computeLeadingNulls..size-computeTrailingNulls).
+ *
+ * This allows the diff of a PagedList, e.g.:
+ *     100 nulls, placeholder page, (empty page) x 5, page, 100 nulls
+ *
+ * To only inform DiffUtil about single loaded page in this case, by pruning all other nulls from
+ * consideration.
+ *
+ * @see PagedStorage#computeLeadingNulls()
+ * @see PagedStorage#computeTrailingNulls()
+ */
 class PagedStorageDiffHelper {
     private PagedStorageDiffHelper() {
     }
@@ -178,12 +193,16 @@
      */
     static int transformAnchorIndex(@NonNull DiffUtil.DiffResult diffResult,
             @NonNull PagedStorage oldList, @NonNull PagedStorage newList, final int oldPosition) {
+        final int oldOffset = oldList.computeLeadingNulls();
+
         // diffResult's indices starting after nulls, need to transform to diffutil indices
         // (see also dispatchDiff(), which adds this offset when dispatching)
-        int diffIndex = oldPosition - oldList.getLeadingNullCount();
+        int diffIndex = oldPosition - oldOffset;
+
+        final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls();
 
         // if our anchor is non-null, use it or close item's position in new list
-        if (diffIndex >= 0 && diffIndex < oldList.getStorageCount()) {
+        if (diffIndex >= 0 && diffIndex < oldSize) {
             // search outward from old position for position that maps
             for (int i = 0; i < 30; i++) {
                 int positionToTry = diffIndex + (i / 2 * (i % 2 == 1 ? -1 : 1));