LimitOffsetPagingSource to return LoadResult.Error

Currently any Exception resulting from calling LimitOffsetPagingSource.load isn't caught, resulting in app crashes unless users caught it as part of the db query. Now we return LoadResult.Error instead which is propagated as a LoadStateUpdate. This also makes it easier to recover from errors.

Test: ./gradlew room:room-paging:cC
Bug: 302708983
Relnote: "Exceptions thrown from PagingSource loads will now be propagated as a LoadStateUpdate of LoadResult.Error containing the Throwable. This error state is observable through PagingDataAdapter.loadStateFlow(Views) or LazyPagingItems.loadState(Compose). Note that this marks a behavioral change where in the past load errors will bubble up as an Exception thrown by the dao method that triggered the load."

Change-Id: I93887de62a4fc76d1abe4ade60ed524a41d9d4e8
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index f818b98..739624f 100644
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -36,7 +36,6 @@
 import androidx.testutils.TestExecutor
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
-import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 import kotlinx.coroutines.Dispatchers
@@ -315,6 +314,21 @@
     }
 
     @Test
+    fun load_invalidQuery() = runPagingSourceTest(
+        LimitOffsetPagingSourceImpl(
+            db = database,
+            queryString = "SELECT * FROM $tableName ORDER BY",
+        )
+    ) { pager, _ ->
+        dao.addAllItems(ITEMS_LIST)
+        val result = pager.refresh()
+
+        assertThat(result).isInstanceOf<LoadResult.Error<Int, Int>>()
+        val throwable = (result as LoadResult.Error).throwable
+        assertThat(throwable).isNotNull()
+    }
+
+    @Test
     fun invalidInitialKey_dbEmpty_returnsEmpty() = runPagingSourceTest { pager, _ ->
         assertThat(
             (pager.refresh(initialKey = 101) as LoadResult.Page).data
@@ -337,12 +351,12 @@
     @Test
     fun invalidInitialKey_negativeKey() = runPagingSourceTest { pager, _ ->
         dao.addAllItems(ITEMS_LIST)
-        // should throw error when initial key is negative
-        val expectedException = assertFailsWith<IllegalArgumentException> {
-            pager.refresh(initialKey = -1)
-        }
+        // should return error when initial key is negative
+        val result = pager.refresh(initialKey = -1)
+        assertThat(result).isInstanceOf<LoadResult.Error<Int, Int>>()
+        val message = (result as LoadResult.Error).throwable.message
         // default message from Paging 3 for negative initial key
-        assertThat(expectedException.message).isEqualTo(
+        assertThat(message).isEqualTo(
             "itemsBefore cannot be negative"
         )
     }
diff --git a/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt
index bca76d1..eae82a6 100644
--- a/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/main/java/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -71,10 +71,14 @@
             observer.registerIfNecessary(db)
             val tempCount = itemCount.get()
             // if itemCount is < 0, then it is initial load
-            if (tempCount == INITIAL_ITEM_COUNT) {
-                initialLoad(params)
-            } else {
-                nonInitialLoad(params, tempCount)
+            try {
+                if (tempCount == INITIAL_ITEM_COUNT) {
+                    initialLoad(params)
+                } else {
+                    nonInitialLoad(params, tempCount)
+                }
+            } catch (e: Exception) {
+                LoadResult.Error(e)
             }
         }
     }