Fallback to ShortcutManager if AppPredictor has crashed

Bug: 269230501
Test: unit tests
Change-Id: If5ac83d0f301bda8d00721f56ed8df88332a147f
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
index 6f7542f..29e706d 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
@@ -77,6 +77,7 @@
     private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
     private val activeRequest = AtomicReference(NO_REQUEST)
     private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) }
+    @Volatile
     private var isDestroyed = false
 
     @MainThread
@@ -134,8 +135,14 @@
     @WorkerThread
     private fun queryDirectShareTargets(skipAppPredictionService: Boolean) {
         if (!skipAppPredictionService && appPredictor != null) {
-            appPredictor.requestPredictionUpdate()
-            return
+            try {
+                appPredictor.requestPredictionUpdate()
+                return
+            } catch (e: Throwable) {
+                // we might have been destroyed concurrently, nothing left to do
+                if (isDestroyed) return
+                Log.e(TAG, "Failed to query AppPredictor", e)
+            }
         }
         // Default to just querying ShortcutManager if AppPredictor not present.
         if (targetIntentFilter == null) return
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
index 0c817cb..e8e2f86 100644
--- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
+++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
@@ -58,7 +58,7 @@
     private val pm = mock<PackageManager> {
         whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
     }
-    val userManager = mock<UserManager> {
+    private val userManager = mock<UserManager> {
         whenever(isUserRunning(any<UserHandle>())).thenReturn(true)
         whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true)
         whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false)
@@ -72,14 +72,15 @@
     private val intentFilter = mock<IntentFilter>()
     private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
     private val callback = mock<Consumer<ShortcutLoader.Result>>()
+    private val componentName = ComponentName("pkg", "Class")
+    private val appTarget = mock<DisplayResolveInfo> {
+        whenever(resolvedComponentName).thenReturn(componentName)
+    }
+    private val appTargets = arrayOf(appTarget)
+    private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
 
     @Test
     fun test_queryShortcuts_result_consistency_with_AppPredictor() {
-        val componentName = ComponentName("pkg", "Class")
-        val appTarget = mock<DisplayResolveInfo> {
-            whenever(resolvedComponentName).thenReturn(componentName)
-        }
-        val appTargets = arrayOf(appTarget)
         val testSubject = ShortcutLoader(
             context,
             appPredictor,
@@ -93,7 +94,6 @@
 
         testSubject.queryShortcuts(appTargets)
 
-        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
         val matchingAppTarget = createAppTarget(matchingShortcutInfo)
         val shortcuts = listOf(
             matchingAppTarget,
@@ -131,12 +131,6 @@
 
     @Test
     fun test_queryShortcuts_result_consistency_with_ShortcutManager() {
-        val componentName = ComponentName("pkg", "Class")
-        val appTarget = mock<DisplayResolveInfo> {
-            whenever(resolvedComponentName).thenReturn(componentName)
-        }
-        val appTargets = arrayOf(appTarget)
-        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
         val shortcutManagerResult = listOf(
             ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
             // mismatching shortcut
@@ -182,12 +176,6 @@
 
     @Test
     fun test_queryShortcuts_falls_back_to_ShortcutManager_on_empty_reply() {
-        val componentName = ComponentName("pkg", "Class")
-        val appTarget = mock<DisplayResolveInfo> {
-            whenever(resolvedComponentName).thenReturn(componentName)
-        }
-        val appTargets = arrayOf(appTarget)
-        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
         val shortcutManagerResult = listOf(
             ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
             // mismatching shortcut
@@ -238,6 +226,55 @@
     }
 
     @Test
+    fun test_queryShortcuts_onAppPredictorFailure_fallbackToShortcutManager() {
+        val shortcutManagerResult = listOf(
+            ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+            // mismatching shortcut
+            createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+        )
+        val shortcutManager = mock<ShortcutManager> {
+            whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+        }
+        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+        whenever(appPredictor.requestPredictionUpdate())
+            .thenThrow(IllegalStateException("Test exception"))
+        val testSubject = ShortcutLoader(
+            context,
+            appPredictor,
+            UserHandle.of(0),
+            true,
+            intentFilter,
+            executor,
+            executor,
+            callback
+        )
+
+        testSubject.queryShortcuts(appTargets)
+
+        verify(appPredictor, times(1)).requestPredictionUpdate()
+
+        val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+        verify(callback, times(1)).accept(capture(resultCaptor))
+
+        val result = resultCaptor.value
+        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
+        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+        for (shortcut in result.shortcutsByApp[0].shortcuts) {
+            assertTrue(
+                "AppTargets are not expected the cache of a ShortcutManager result",
+                result.directShareAppTargetCache.isEmpty()
+            )
+            assertEquals(
+                "Wrong ShortcutInfo in the cache",
+                matchingShortcutInfo,
+                result.directShareShortcutInfoCache[shortcut]
+            )
+        }
+    }
+
+    @Test
     fun test_queryShortcuts_do_not_call_services_for_not_running_work_profile() {
         testDisabledWorkProfileDoNotCallSystem(isUserRunning = false)
     }