Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5fb1e0aa authored by Lily Zhou's avatar Lily Zhou Committed by Android (Google) Code Review
Browse files

Merge "Creates a service in SysUI (WalletContextualLocationsService) that can...

Merge "Creates a service in SysUI (WalletContextualLocationsService) that can send store locations to AiAi." into udc-dev
parents dd33ac87 2bd65e09
Loading
Loading
Loading
Loading
+3 −0
Original line number Original line Diff line number Diff line
@@ -380,6 +380,9 @@
        <service android:name="SystemUIService"
        <service android:name="SystemUIService"
            android:exported="true"
            android:exported="true"
        />
        />
        <service android:name=".wallet.controller.WalletContextualLocationsService"
            android:exported="true"
            />


        <!-- Service for dumping extremely verbose content during a bug report -->
        <!-- Service for dumping extremely verbose content during a bug report -->
        <service android:name=".dump.SystemUIAuxiliaryDumpService"
        <service android:name=".dump.SystemUIAuxiliaryDumpService"
+93 −0
Original line number Original line Diff line number Diff line
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
+5 −2
Original line number Original line Diff line number Diff line
@@ -36,6 +36,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.emptyFlow
@@ -57,7 +58,8 @@ constructor(
) {
) {
    private val cardsReceivedCallbacks: MutableSet<(List<WalletCard>) -> Unit> = mutableSetOf()
    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)) {
        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
            // 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.
            // too frequently. Also check if the list actually changed before calling callbacks.
@@ -107,12 +109,13 @@ constructor(
                    emptyList()
                    emptyList()
                )
                )
        } else {
        } else {
            emptyFlow()
            MutableStateFlow<List<WalletCard>>(emptyList()).asStateFlow()
        }
        }


    private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet())
    private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet())
    private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow()
    private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow()


    /** Contextually-relevant cards. */
    val contextualSuggestionCards: Flow<List<WalletCard>> =
    val contextualSuggestionCards: Flow<List<WalletCard>> =
        combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
        combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
                val ret =
                val ret =
+8 −0
Original line number Original line Diff line number Diff line
@@ -35,6 +35,8 @@ import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import dagger.multibindings.StringKey;


import android.app.Service;
import com.android.systemui.wallet.controller.WalletContextualLocationsService;


/**
/**
 * Module for injecting classes in Wallet.
 * Module for injecting classes in Wallet.
@@ -42,6 +44,12 @@ import dagger.multibindings.StringKey;
@Module
@Module
public abstract class WalletModule {
public abstract class WalletModule {


    @Binds
    @IntoMap
    @ClassKey(WalletContextualLocationsService.class)
    abstract Service bindWalletContextualLocationsService(
        WalletContextualLocationsService service);

    /** */
    /** */
    @Binds
    @Binds
    @IntoMap
    @IntoMap
+128 −0
Original line number Original line Diff line number Diff line
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))
        }
}
 No newline at end of file