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