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

to AiAi.

Bug: 270394156
Change-Id: I5e67705cd38db55ecd627c7c2358a6a2815b9587
This commit is contained in:
Lily Zhou
2023-03-22 20:57:49 +00:00
parent 68e0f1c2bf
commit 2bd65e09d8
5 changed files with 237 additions and 2 deletions

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -36,6 +36,7 @@ import kotlinx.coroutines.channels.awaitClose
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 @@ constructor(
) {
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 @@ constructor(
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 =

View File

@@ -35,6 +35,8 @@ import dagger.multibindings.ClassKey;
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 @@ import dagger.multibindings.StringKey;
@Module
public abstract class WalletModule {
@Binds
@IntoMap
@ClassKey(WalletContextualLocationsService.class)
abstract Service bindWalletContextualLocationsService(
WalletContextualLocationsService service);
/** */
@Binds
@IntoMap

View File

@@ -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
@kotlinx.coroutines.ExperimentalCoroutinesApi
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))
}
}