Creates a service in SysUI (WalletContextualLocationsService) that can send store locations
to AiAi.

Bug: 270394156
Change-Id: I5e67705cd38db55ecd627c7c2358a6a2815b9587
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 09c62d0..f4e5692 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -389,6 +389,9 @@
         <service android:name="SystemUIService"
             android:exported="true"
         />
+        <service android:name=".wallet.controller.WalletContextualLocationsService"
+            android:exported="true"
+            />
 
         <!-- Service for dumping extremely verbose content during a bug report -->
         <service android:name=".dump.SystemUIAuxiliaryDumpService"
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
new file mode 100644
index 0000000..1c17fc3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
@@ -0,0 +1,93 @@
+package com.android.systemui.wallet.controller
+
+import android.content.Intent
+import android.os.IBinder
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Serves as an intermediary between QuickAccessWalletService and ContextualCardManager (in PCC).
+ * When QuickAccessWalletService has a list of store locations, WalletContextualLocationsService
+ * will send them to ContextualCardManager. When the user enters a store location, this Service
+ * class will be notified, and WalletContextualSuggestionsController will be updated.
+ */
+class WalletContextualLocationsService
+@Inject
+constructor(
+    private val controller: WalletContextualSuggestionsController,
+    private val featureFlags: FeatureFlags,
+) : LifecycleService() {
+    private var listener: IWalletCardsUpdatedListener? = null
+    private var scope: CoroutineScope = this.lifecycleScope
+
+    @VisibleForTesting
+    constructor(
+        controller: WalletContextualSuggestionsController,
+        featureFlags: FeatureFlags,
+        scope: CoroutineScope,
+    ) : this(controller, featureFlags) {
+        this.scope = scope
+    }
+
+    override fun onBind(intent: Intent): IBinder {
+        super.onBind(intent)
+        scope.launch {
+            controller.allWalletCards.collect { cards ->
+                val cardsSize = cards.size
+                Log.i(TAG, "Number of cards registered $cardsSize")
+                listener?.registerNewWalletCards(cards)
+            }
+        }
+        return binder
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        listener = null
+    }
+
+    @VisibleForTesting
+    fun addWalletCardsUpdatedListenerInternal(listener: IWalletCardsUpdatedListener) {
+        if (!featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+            return
+        }
+        this.listener = listener // Currently, only one listener at a time is supported
+        // Sends WalletCard objects from QuickAccessWalletService to the listener
+        val cards = controller.allWalletCards.value
+        if (!cards.isEmpty()) {
+            val cardsSize = cards.size
+            Log.i(TAG, "Number of cards registered $cardsSize")
+            listener.registerNewWalletCards(cards)
+        }
+    }
+
+    @VisibleForTesting
+    fun onWalletContextualLocationsStateUpdatedInternal(storeLocations: List<String>) {
+        if (!featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+            return
+        }
+        Log.i(TAG, "Entered store $storeLocations")
+        controller.setSuggestionCardIds(storeLocations.toSet())
+    }
+
+    private val binder: IWalletContextualLocationsService.Stub
+    = object : IWalletContextualLocationsService.Stub() {
+        override fun addWalletCardsUpdatedListener(listener: IWalletCardsUpdatedListener) {
+            addWalletCardsUpdatedListenerInternal(listener)
+        }
+        override fun onWalletContextualLocationsStateUpdated(storeLocations: List<String>) {
+            onWalletContextualLocationsStateUpdatedInternal(storeLocations)
+        }
+    }
+
+    companion object {
+        private const val TAG = "WalletContextualLocationsService"
+    }
+}
\ No newline at end of file
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 518f5a7..b3ad9b0 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
@@ -36,6 +36,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.emptyFlow
@@ -57,7 +58,8 @@
 ) {
     private val cardsReceivedCallbacks: MutableSet<(List<WalletCard>) -> Unit> = mutableSetOf()
 
-    private val allWalletCards: Flow<List<WalletCard>> =
+    /** All potential cards. */
+    val allWalletCards: StateFlow<List<WalletCard>> =
         if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
             // 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.
@@ -107,12 +109,13 @@
                     emptyList()
                 )
         } else {
-            emptyFlow()
+            MutableStateFlow<List<WalletCard>>(emptyList()).asStateFlow()
         }
 
     private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet())
     private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow()
 
+    /** Contextually-relevant cards. */
     val contextualSuggestionCards: Flow<List<WalletCard>> =
         combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
                 val ret =
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
index 9429d89..efba3e5 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
@@ -35,6 +35,8 @@
 import dagger.multibindings.IntoMap;
 import dagger.multibindings.StringKey;
 
+import android.app.Service;
+import com.android.systemui.wallet.controller.WalletContextualLocationsService;
 
 /**
  * Module for injecting classes in Wallet.
@@ -42,6 +44,12 @@
 @Module
 public abstract class WalletModule {
 
+    @Binds
+    @IntoMap
+    @ClassKey(WalletContextualLocationsService.class)
+    abstract Service bindWalletContextualLocationsService(
+        WalletContextualLocationsService service);
+
     /** */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt
new file mode 100644
index 0000000..af1d788
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt
@@ -0,0 +1,128 @@
+package com.android.systemui.wallet.controller
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Looper
+import android.service.quickaccesswallet.WalletCard
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.anySet
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(JUnit4::class)
+@SmallTest
[email protected]
+class WalletContextualLocationsServiceTest : SysuiTestCase() {
+    @Mock private lateinit var controller: WalletContextualSuggestionsController
+    private var featureFlags = FakeFeatureFlags()
+    private lateinit var underTest: WalletContextualLocationsService
+    private lateinit var testScope: TestScope
+    private var listenerRegisteredCount: Int = 0
+    private val listener: IWalletCardsUpdatedListener.Stub = object : IWalletCardsUpdatedListener.Stub() {
+        override fun registerNewWalletCards(cards: List<WalletCard?>) {
+            listenerRegisteredCount++
+        }
+    }
+
+    @Before
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        doReturn(fakeWalletCards).whenever(controller).allWalletCards
+        doNothing().whenever(controller).setSuggestionCardIds(anySet())
+
+        if (Looper.myLooper() == null) Looper.prepare()
+
+        testScope = TestScope()
+        featureFlags.set(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS, true)
+        listenerRegisteredCount = 0
+
+        underTest = WalletContextualLocationsService(controller, featureFlags, testScope.backgroundScope)
+    }
+
+    @Test
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun addListener() = testScope.runTest {
+        underTest.addWalletCardsUpdatedListenerInternal(listener)
+        assertThat(listenerRegisteredCount).isEqualTo(1)
+  }
+
+    @Test
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun addStoreLocations() = testScope.runTest {
+        underTest.onWalletContextualLocationsStateUpdatedInternal(ArrayList<String>())
+        verify(controller, times(1)).setSuggestionCardIds(anySet())
+    }
+
+    @Test
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun updateListenerAndLocationsState() = testScope.runTest {
+        // binds to the service and adds a listener
+        val underTestStub = getInterface
+        underTestStub.addWalletCardsUpdatedListener(listener)
+        assertThat(listenerRegisteredCount).isEqualTo(1)
+
+        // sends a list of card IDs to the controller
+        underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>())
+        verify(controller, times(1)).setSuggestionCardIds(anySet())
+
+        // adds another listener
+        fakeWalletCards.update{ updatedFakeWalletCards }
+        runCurrent()
+        assertThat(listenerRegisteredCount).isEqualTo(2)
+
+        // sends another list of card IDs to the controller
+        underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>())
+        verify(controller, times(2)).setSuggestionCardIds(anySet())
+    }
+
+    private val fakeWalletCards: MutableStateFlow<List<WalletCard>>
+        get() {
+            val intent = Intent(getContext(), WalletContextualLocationsService::class.java)
+            val pi: PendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE)
+            val icon: Icon = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888))
+            val walletCards: ArrayList<WalletCard> = ArrayList<WalletCard>()
+            walletCards.add(WalletCard.Builder("card1", icon, "card", pi).build())
+            walletCards.add(WalletCard.Builder("card2", icon, "card", pi).build())
+            return MutableStateFlow<List<WalletCard>>(walletCards)
+        }
+
+    private val updatedFakeWalletCards: List<WalletCard>
+        get() {
+            val intent = Intent(getContext(), WalletContextualLocationsService::class.java)
+            val pi: PendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE)
+            val icon: Icon = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888))
+            val walletCards: ArrayList<WalletCard> = ArrayList<WalletCard>()
+            walletCards.add(WalletCard.Builder("card3", icon, "card", pi).build())
+            return walletCards
+        }
+
+    private val getInterface: IWalletContextualLocationsService
+        get() {
+            val intent = Intent()
+            return IWalletContextualLocationsService.Stub.asInterface(underTest.onBind(intent))
+        }
+}
\ No newline at end of file