Merge "Update wallet suggestions controller to no longer use a broadcast for suggestions" into udc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
index 7b8235a..518f5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
@@ -16,8 +16,7 @@
 
 package com.android.systemui.wallet.controller
 
-import android.Manifest
-import android.content.Context
+import android.content.Intent
 import android.content.IntentFilter
 import android.service.quickaccesswallet.GetWalletCardsError
 import android.service.quickaccesswallet.GetWalletCardsResponse
@@ -32,13 +31,21 @@
 import com.android.systemui.flags.Flags
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class WalletContextualSuggestionsController
 @Inject
@@ -48,68 +55,99 @@
     broadcastDispatcher: BroadcastDispatcher,
     featureFlags: FeatureFlags
 ) {
+    private val cardsReceivedCallbacks: MutableSet<(List<WalletCard>) -> Unit> = mutableSetOf()
+
     private val allWalletCards: Flow<List<WalletCard>> =
         if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
-            conflatedCallbackFlow {
-                val callback =
-                    object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
-                        override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
-                            trySendWithFailureLogging(response.walletCards, TAG)
-                        }
+            // TODO(b/237409756) determine if we should debounce this so we don't call the service
+            // too frequently. Also check if the list actually changed before calling callbacks.
+            broadcastDispatcher
+                .broadcastFlow(IntentFilter(Intent.ACTION_SCREEN_ON))
+                .flatMapLatest {
+                    conflatedCallbackFlow {
+                        val callback =
+                            object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
+                                override fun onWalletCardsRetrieved(
+                                    response: GetWalletCardsResponse
+                                ) {
+                                    trySendWithFailureLogging(response.walletCards, TAG)
+                                }
 
-                        override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
-                            trySendWithFailureLogging(emptyList<WalletCard>(), TAG)
+                                override fun onWalletCardRetrievalError(
+                                    error: GetWalletCardsError
+                                ) {
+                                    trySendWithFailureLogging(emptyList<WalletCard>(), TAG)
+                                }
+                            }
+
+                        walletController.setupWalletChangeObservers(
+                            callback,
+                            QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+                            QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+                        )
+                        walletController.updateWalletPreference()
+                        walletController.queryWalletCards(callback)
+
+                        awaitClose {
+                            walletController.unregisterWalletChangeObservers(
+                                QuickAccessWalletController.WalletChangeEvent
+                                    .WALLET_PREFERENCE_CHANGE,
+                                QuickAccessWalletController.WalletChangeEvent
+                                    .DEFAULT_PAYMENT_APP_CHANGE
+                            )
                         }
                     }
-
-                walletController.setupWalletChangeObservers(
-                    callback,
-                    QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
-                    QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+                }
+                .onEach { notifyCallbacks(it) }
+                .stateIn(
+                    applicationCoroutineScope,
+                    // Needs to be done eagerly since we need to notify callbacks even if there are
+                    // no subscribers
+                    SharingStarted.Eagerly,
+                    emptyList()
                 )
-                walletController.updateWalletPreference()
-                walletController.queryWalletCards(callback)
-
-                awaitClose {
-                    walletController.unregisterWalletChangeObservers(
-                        QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
-                        QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
-                    )
-                }
-            }
         } else {
             emptyFlow()
         }
 
-    private val contextualSuggestionsCardIds: Flow<Set<String>> =
-        if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
-            broadcastDispatcher.broadcastFlow(
-                filter = IntentFilter(ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS),
-                permission = Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE,
-                flags = Context.RECEIVER_EXPORTED
-            ) { intent, _ ->
-                if (intent.hasExtra(UPDATE_CARD_IDS_EXTRA)) {
-                    intent.getStringArrayListExtra(UPDATE_CARD_IDS_EXTRA).toSet()
-                } else {
-                    emptySet()
-                }
-            }
-        } else {
-            emptyFlow()
-        }
+    private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet())
+    private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow()
 
     val contextualSuggestionCards: Flow<List<WalletCard>> =
         combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
-                cards.filter { card -> ids.contains(card.cardId) }
+                val ret =
+                    cards.filter { card ->
+                        card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT &&
+                            ids.contains(card.cardId)
+                    }
+                ret
             }
-            .shareIn(applicationCoroutineScope, replay = 1, started = SharingStarted.Eagerly)
+            .stateIn(applicationCoroutineScope, SharingStarted.WhileSubscribed(), emptyList())
+
+    /** When called, {@link contextualSuggestionCards} will be updated to be for these IDs. */
+    fun setSuggestionCardIds(cardIds: Set<String>) {
+        _suggestionCardIds.update { _ -> cardIds }
+    }
+
+    /** Register callback to be called when a new list of cards is fetched. */
+    fun registerWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) {
+        cardsReceivedCallbacks.add(callback)
+    }
+
+    /** Unregister callback to be called when a new list of cards is fetched. */
+    fun unregisterWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) {
+        cardsReceivedCallbacks.remove(callback)
+    }
+
+    private fun notifyCallbacks(cards: List<WalletCard>) {
+        applicationCoroutineScope.launch {
+            cardsReceivedCallbacks.onEach { callback ->
+                callback(cards.filter { card -> card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT })
+            }
+        }
+    }
 
     companion object {
-        private const val ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS =
-            "com.android.systemui.wallet.UPDATE_CONTEXTUAL_SUGGESTIONS"
-
-        private const val UPDATE_CARD_IDS_EXTRA = "cardIds"
-
         private const val TAG = "WalletSuggestions"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt
index b527861..9bd3a79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt
@@ -32,9 +32,9 @@
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import java.util.ArrayList
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
@@ -46,7 +46,7 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.isNull
+import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
@@ -66,12 +66,14 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        whenever(broadcastDispatcher.broadcastFlow(any(), nullable(), anyInt(), nullable()))
+            .thenCallRealMethod()
         whenever(
-                broadcastDispatcher.broadcastFlow<List<String>?>(
+                broadcastDispatcher.broadcastFlow<Unit>(
                     any(),
-                    isNull(),
-                    any(),
-                    any(),
+                    nullable(),
+                    anyInt(),
+                    nullable(),
                     any()
                 )
             )
@@ -81,95 +83,85 @@
             .thenReturn(true)
 
         whenever(CARD_1.cardId).thenReturn(ID_1)
+        whenever(CARD_1.cardType).thenReturn(WalletCard.CARD_TYPE_NON_PAYMENT)
         whenever(CARD_2.cardId).thenReturn(ID_2)
+        whenever(CARD_2.cardType).thenReturn(WalletCard.CARD_TYPE_NON_PAYMENT)
         whenever(CARD_3.cardId).thenReturn(ID_3)
+        whenever(CARD_3.cardType).thenReturn(WalletCard.CARD_TYPE_NON_PAYMENT)
+        whenever(PAYMENT_CARD.cardId).thenReturn(PAYMENT_ID)
+        whenever(PAYMENT_CARD.cardType).thenReturn(WalletCard.CARD_TYPE_PAYMENT)
     }
 
     @Test
-    fun `state - has wallet cards - received contextual cards`() = runTest {
-        setUpWalletClient(listOf(CARD_1, CARD_2))
-        val latest =
-            collectLastValue(
-                createWalletContextualSuggestionsController(backgroundScope)
-                    .contextualSuggestionCards,
-            )
+    fun `state - has wallet cards- callbacks called`() = runTest {
+        setUpWalletClient(listOf(CARD_1, CARD_2, PAYMENT_CARD))
+        val controller = createWalletContextualSuggestionsController(backgroundScope)
+        var latest1 = emptyList<WalletCard>()
+        var latest2 = emptyList<WalletCard>()
+        val callback1: (List<WalletCard>) -> Unit = { latest1 = it }
+        val callback2: (List<WalletCard>) -> Unit = { latest2 = it }
 
         runCurrent()
-        verifyRegistered()
-        broadcastReceiver.value.onReceive(
-            mockContext,
-            createContextualCardsIntent(listOf(ID_1, ID_2))
-        )
+        controller.registerWalletCardsReceivedCallback(callback1)
+        controller.registerWalletCardsReceivedCallback(callback2)
+        controller.unregisterWalletCardsReceivedCallback(callback2)
+        runCurrent()
+        verifyBroadcastReceiverRegistered()
+        turnScreenOn()
+        runCurrent()
 
-        assertThat(latest()).containsExactly(CARD_1, CARD_2)
+        assertThat(latest1).containsExactly(CARD_1, CARD_2)
+        assertThat(latest2).isEmpty()
     }
 
     @Test
-    fun `state - no wallet cards - received contextual cards`() = runTest {
+    fun `state - no wallet cards - set suggestion cards`() = runTest {
         setUpWalletClient(emptyList())
+        val controller = createWalletContextualSuggestionsController(backgroundScope)
         val latest =
             collectLastValue(
-                createWalletContextualSuggestionsController(backgroundScope)
-                    .contextualSuggestionCards,
+                controller.contextualSuggestionCards,
             )
 
         runCurrent()
-        verifyRegistered()
-        broadcastReceiver.value.onReceive(
-            mockContext,
-            createContextualCardsIntent(listOf(ID_1, ID_2))
-        )
+        verifyBroadcastReceiverRegistered()
+        turnScreenOn()
+        controller.setSuggestionCardIds(setOf(ID_1, ID_2))
 
         assertThat(latest()).isEmpty()
     }
 
     @Test
-    fun `state - has wallet cards - no contextual cards`() = runTest {
-        setUpWalletClient(listOf(CARD_1, CARD_2))
+    fun `state - has wallet cards - set and update suggestion cards`() = runTest {
+        setUpWalletClient(listOf(CARD_1, CARD_2, PAYMENT_CARD))
+        val controller = createWalletContextualSuggestionsController(backgroundScope)
         val latest =
             collectLastValue(
-                createWalletContextualSuggestionsController(backgroundScope)
-                    .contextualSuggestionCards,
+                controller.contextualSuggestionCards,
             )
 
         runCurrent()
-        verifyRegistered()
-        broadcastReceiver.value.onReceive(mockContext, createContextualCardsIntent(emptyList()))
+        verifyBroadcastReceiverRegistered()
+        turnScreenOn()
 
+        controller.setSuggestionCardIds(setOf(ID_1, ID_2))
+        assertThat(latest()).containsExactly(CARD_1, CARD_2)
+        controller.setSuggestionCardIds(emptySet())
         assertThat(latest()).isEmpty()
     }
 
     @Test
     fun `state - wallet cards error`() = runTest {
         setUpWalletClient(shouldFail = true)
+        val controller = createWalletContextualSuggestionsController(backgroundScope)
         val latest =
             collectLastValue(
-                createWalletContextualSuggestionsController(backgroundScope)
-                    .contextualSuggestionCards,
+                controller.contextualSuggestionCards,
             )
 
         runCurrent()
-        verifyRegistered()
-        broadcastReceiver.value.onReceive(
-            mockContext,
-            createContextualCardsIntent(listOf(ID_1, ID_2))
-        )
-
-        assertThat(latest()).isEmpty()
-    }
-
-    @Test
-    fun `state - no contextual cards extra`() = runTest {
-        setUpWalletClient(listOf(CARD_1, CARD_2))
-        val latest =
-            collectLastValue(
-                createWalletContextualSuggestionsController(backgroundScope)
-                    .contextualSuggestionCards,
-            )
-
-        runCurrent()
-        verifyRegistered()
-        broadcastReceiver.value.onReceive(mockContext, Intent(INTENT_NAME))
+        verifyBroadcastReceiverRegistered()
+        controller.setSuggestionCardIds(setOf(ID_1, ID_2))
 
         assertThat(latest()).isEmpty()
     }
@@ -178,16 +170,18 @@
     fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest {
         whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)))
             .thenReturn(false)
-        setUpWalletClient(listOf(CARD_1, CARD_2))
+        setUpWalletClient(listOf(CARD_1, CARD_2, PAYMENT_CARD))
+        val controller = createWalletContextualSuggestionsController(backgroundScope)
         val latest =
             collectLastValue(
-                createWalletContextualSuggestionsController(backgroundScope)
-                    .contextualSuggestionCards,
+                controller.contextualSuggestionCards,
             )
 
         runCurrent()
-        verify(broadcastDispatcher, never()).broadcastFlow(any(), isNull(), any(), any())
-        assertThat(latest()).isNull()
+        verify(broadcastDispatcher, never()).broadcastFlow(any(), nullable(), anyInt(), nullable())
+        controller.setSuggestionCardIds(setOf(ID_1, ID_2))
+
+        assertThat(latest()).isEmpty()
     }
 
     private fun createWalletContextualSuggestionsController(
@@ -201,17 +195,20 @@
         )
     }
 
-    private fun verifyRegistered() {
+    private fun verifyBroadcastReceiverRegistered() {
         verify(broadcastDispatcher)
-            .registerReceiver(capture(broadcastReceiver), any(), isNull(), isNull(), any(), any())
+            .registerReceiver(
+                capture(broadcastReceiver),
+                any(),
+                nullable(),
+                nullable(),
+                anyInt(),
+                nullable()
+            )
     }
 
-    private fun createContextualCardsIntent(
-        ids: List<String> = emptyList(),
-    ): Intent {
-        val intent = Intent(INTENT_NAME)
-        intent.putStringArrayListExtra("cardIds", ArrayList(ids))
-        return intent
+    private fun turnScreenOn() {
+        broadcastReceiver.value.onReceive(mockContext, Intent(Intent.ACTION_SCREEN_ON))
     }
 
     private fun setUpWalletClient(
@@ -238,6 +235,7 @@
         private val CARD_2: WalletCard = mock()
         private const val ID_3: String = "789"
         private val CARD_3: WalletCard = mock()
-        private val INTENT_NAME: String = "WalletSuggestionsIntent"
+        private const val PAYMENT_ID: String = "payment"
+        private val PAYMENT_CARD: WalletCard = mock()
     }
 }