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

Commit a38d32bb authored by John Johnson's avatar John Johnson Committed by Android (Google) Code Review
Browse files

Merge "Update wallet suggestions controller to no longer use a broadcast for...

Merge "Update wallet suggestions controller to no longer use a broadcast for suggestions" into udc-dev
parents 7b05ea66 419475c5
Loading
Loading
Loading
Loading
+86 −48
Original line number Original line Diff line number Diff line
@@ -16,8 +16,7 @@


package com.android.systemui.wallet.controller
package com.android.systemui.wallet.controller


import android.Manifest
import android.content.Intent
import android.content.Context
import android.content.IntentFilter
import android.content.IntentFilter
import android.service.quickaccesswallet.GetWalletCardsError
import android.service.quickaccesswallet.GetWalletCardsError
import android.service.quickaccesswallet.GetWalletCardsResponse
import android.service.quickaccesswallet.GetWalletCardsResponse
@@ -32,13 +31,21 @@ import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.flags.Flags
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharingStarted
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
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
@SysUISingleton
class WalletContextualSuggestionsController
class WalletContextualSuggestionsController
@Inject
@Inject
@@ -48,16 +55,27 @@ constructor(
    broadcastDispatcher: BroadcastDispatcher,
    broadcastDispatcher: BroadcastDispatcher,
    featureFlags: FeatureFlags
    featureFlags: FeatureFlags
) {
) {
    private val cardsReceivedCallbacks: MutableSet<(List<WalletCard>) -> Unit> = mutableSetOf()

    private val allWalletCards: Flow<List<WalletCard>> =
    private val allWalletCards: Flow<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
            // too frequently. Also check if the list actually changed before calling callbacks.
            broadcastDispatcher
                .broadcastFlow(IntentFilter(Intent.ACTION_SCREEN_ON))
                .flatMapLatest {
                    conflatedCallbackFlow {
                    conflatedCallbackFlow {
                        val callback =
                        val callback =
                            object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
                            object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
                        override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
                                override fun onWalletCardsRetrieved(
                                    response: GetWalletCardsResponse
                                ) {
                                    trySendWithFailureLogging(response.walletCards, TAG)
                                    trySendWithFailureLogging(response.walletCards, TAG)
                                }
                                }


                        override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
                                override fun onWalletCardRetrievalError(
                                    error: GetWalletCardsError
                                ) {
                                    trySendWithFailureLogging(emptyList<WalletCard>(), TAG)
                                    trySendWithFailureLogging(emptyList<WalletCard>(), TAG)
                                }
                                }
                            }
                            }
@@ -72,44 +90,64 @@ constructor(


                        awaitClose {
                        awaitClose {
                            walletController.unregisterWalletChangeObservers(
                            walletController.unregisterWalletChangeObservers(
                        QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
                                QuickAccessWalletController.WalletChangeEvent
                        QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
                                    .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()
                )
        } else {
        } else {
            emptyFlow()
            emptyFlow()
        }
        }


    private val contextualSuggestionsCardIds: Flow<Set<String>> =
    private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet())
        if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
    private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow()
            broadcastDispatcher.broadcastFlow(

                filter = IntentFilter(ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS),
    val contextualSuggestionCards: Flow<List<WalletCard>> =
                permission = Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE,
        combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
                flags = Context.RECEIVER_EXPORTED
                val ret =
            ) { intent, _ ->
                    cards.filter { card ->
                if (intent.hasExtra(UPDATE_CARD_IDS_EXTRA)) {
                        card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT &&
                    intent.getStringArrayListExtra(UPDATE_CARD_IDS_EXTRA).toSet()
                            ids.contains(card.cardId)
                } else {
                    emptySet()
                    }
                    }
                ret
            }
            }
        } else {
            .stateIn(applicationCoroutineScope, SharingStarted.WhileSubscribed(), emptyList())
            emptyFlow()

    /** When called, {@link contextualSuggestionCards} will be updated to be for these IDs. */
    fun setSuggestionCardIds(cardIds: Set<String>) {
        _suggestionCardIds.update { _ -> cardIds }
    }
    }


    val contextualSuggestionCards: Flow<List<WalletCard>> =
    /** Register callback to be called when a new list of cards is fetched. */
        combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
    fun registerWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) {
                cards.filter { card -> ids.contains(card.cardId) }
        cardsReceivedCallbacks.add(callback)
    }
    }
            .shareIn(applicationCoroutineScope, replay = 1, started = SharingStarted.Eagerly)


    companion object {
    /** Unregister callback to be called when a new list of cards is fetched. */
        private const val ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS =
    fun unregisterWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) {
            "com.android.systemui.wallet.UPDATE_CONTEXTUAL_SUGGESTIONS"
        cardsReceivedCallbacks.remove(callback)
    }


        private const val UPDATE_CARD_IDS_EXTRA = "cardIds"
    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 TAG = "WalletSuggestions"
        private const val TAG = "WalletSuggestions"
    }
    }
}
}
+68 −70
Original line number Original line Diff line number Diff line
@@ -32,9 +32,9 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertThat
import java.util.ArrayList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runCurrent
@@ -46,7 +46,7 @@ import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mock
import org.mockito.Mockito.isNull
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.never
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.MockitoAnnotations
@@ -66,12 +66,14 @@ class WalletContextualSuggestionsControllerTest : SysuiTestCase() {
    fun setUp() {
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        MockitoAnnotations.initMocks(this)


        whenever(broadcastDispatcher.broadcastFlow(any(), nullable(), anyInt(), nullable()))
            .thenCallRealMethod()
        whenever(
        whenever(
                broadcastDispatcher.broadcastFlow<List<String>?>(
                broadcastDispatcher.broadcastFlow<Unit>(
                    any(),
                    isNull(),
                    any(),
                    any(),
                    any(),
                    nullable(),
                    anyInt(),
                    nullable(),
                    any()
                    any()
                )
                )
            )
            )
@@ -81,95 +83,85 @@ class WalletContextualSuggestionsControllerTest : SysuiTestCase() {
            .thenReturn(true)
            .thenReturn(true)


        whenever(CARD_1.cardId).thenReturn(ID_1)
        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.cardId).thenReturn(ID_2)
        whenever(CARD_2.cardType).thenReturn(WalletCard.CARD_TYPE_NON_PAYMENT)
        whenever(CARD_3.cardId).thenReturn(ID_3)
        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
    @Test
    fun `state - has wallet cards - received contextual cards`() = runTest {
    fun `state - has wallet cards- callbacks called`() = runTest {
        setUpWalletClient(listOf(CARD_1, CARD_2))
        setUpWalletClient(listOf(CARD_1, CARD_2, PAYMENT_CARD))
        val latest =
        val controller = createWalletContextualSuggestionsController(backgroundScope)
            collectLastValue(
        var latest1 = emptyList<WalletCard>()
                createWalletContextualSuggestionsController(backgroundScope)
        var latest2 = emptyList<WalletCard>()
                    .contextualSuggestionCards,
        val callback1: (List<WalletCard>) -> Unit = { latest1 = it }
            )
        val callback2: (List<WalletCard>) -> Unit = { latest2 = it }


        runCurrent()
        runCurrent()
        verifyRegistered()
        controller.registerWalletCardsReceivedCallback(callback1)
        broadcastReceiver.value.onReceive(
        controller.registerWalletCardsReceivedCallback(callback2)
            mockContext,
        controller.unregisterWalletCardsReceivedCallback(callback2)
            createContextualCardsIntent(listOf(ID_1, ID_2))
        runCurrent()
        )
        verifyBroadcastReceiverRegistered()
        turnScreenOn()
        runCurrent()


        assertThat(latest()).containsExactly(CARD_1, CARD_2)
        assertThat(latest1).containsExactly(CARD_1, CARD_2)
        assertThat(latest2).isEmpty()
    }
    }


    @Test
    @Test
    fun `state - no wallet cards - received contextual cards`() = runTest {
    fun `state - no wallet cards - set suggestion cards`() = runTest {
        setUpWalletClient(emptyList())
        setUpWalletClient(emptyList())
        val controller = createWalletContextualSuggestionsController(backgroundScope)
        val latest =
        val latest =
            collectLastValue(
            collectLastValue(
                createWalletContextualSuggestionsController(backgroundScope)
                controller.contextualSuggestionCards,
                    .contextualSuggestionCards,
            )
            )


        runCurrent()
        runCurrent()
        verifyRegistered()
        verifyBroadcastReceiverRegistered()
        broadcastReceiver.value.onReceive(
        turnScreenOn()
            mockContext,
        controller.setSuggestionCardIds(setOf(ID_1, ID_2))
            createContextualCardsIntent(listOf(ID_1, ID_2))
        )


        assertThat(latest()).isEmpty()
        assertThat(latest()).isEmpty()
    }
    }


    @Test
    @Test
    fun `state - has wallet cards - no contextual cards`() = runTest {
    fun `state - has wallet cards - set and update suggestion cards`() = runTest {
        setUpWalletClient(listOf(CARD_1, CARD_2))
        setUpWalletClient(listOf(CARD_1, CARD_2, PAYMENT_CARD))
        val controller = createWalletContextualSuggestionsController(backgroundScope)
        val latest =
        val latest =
            collectLastValue(
            collectLastValue(
                createWalletContextualSuggestionsController(backgroundScope)
                controller.contextualSuggestionCards,
                    .contextualSuggestionCards,
            )
            )


        runCurrent()
        runCurrent()
        verifyRegistered()
        verifyBroadcastReceiverRegistered()
        broadcastReceiver.value.onReceive(mockContext, createContextualCardsIntent(emptyList()))
        turnScreenOn()


        controller.setSuggestionCardIds(setOf(ID_1, ID_2))
        assertThat(latest()).containsExactly(CARD_1, CARD_2)
        controller.setSuggestionCardIds(emptySet())
        assertThat(latest()).isEmpty()
        assertThat(latest()).isEmpty()
    }
    }


    @Test
    @Test
    fun `state - wallet cards error`() = runTest {
    fun `state - wallet cards error`() = runTest {
        setUpWalletClient(shouldFail = true)
        setUpWalletClient(shouldFail = true)
        val controller = createWalletContextualSuggestionsController(backgroundScope)
        val latest =
        val latest =
            collectLastValue(
            collectLastValue(
                createWalletContextualSuggestionsController(backgroundScope)
                controller.contextualSuggestionCards,
                    .contextualSuggestionCards,
            )
            )


        runCurrent()
        runCurrent()
        verifyRegistered()
        verifyBroadcastReceiverRegistered()
        broadcastReceiver.value.onReceive(
        controller.setSuggestionCardIds(setOf(ID_1, ID_2))
            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))


        assertThat(latest()).isEmpty()
        assertThat(latest()).isEmpty()
    }
    }
@@ -178,16 +170,18 @@ class WalletContextualSuggestionsControllerTest : SysuiTestCase() {
    fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest {
    fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest {
        whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)))
        whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)))
            .thenReturn(false)
            .thenReturn(false)
        setUpWalletClient(listOf(CARD_1, CARD_2))
        setUpWalletClient(listOf(CARD_1, CARD_2, PAYMENT_CARD))
        val controller = createWalletContextualSuggestionsController(backgroundScope)
        val latest =
        val latest =
            collectLastValue(
            collectLastValue(
                createWalletContextualSuggestionsController(backgroundScope)
                controller.contextualSuggestionCards,
                    .contextualSuggestionCards,
            )
            )


        runCurrent()
        runCurrent()
        verify(broadcastDispatcher, never()).broadcastFlow(any(), isNull(), any(), any())
        verify(broadcastDispatcher, never()).broadcastFlow(any(), nullable(), anyInt(), nullable())
        assertThat(latest()).isNull()
        controller.setSuggestionCardIds(setOf(ID_1, ID_2))

        assertThat(latest()).isEmpty()
    }
    }


    private fun createWalletContextualSuggestionsController(
    private fun createWalletContextualSuggestionsController(
@@ -201,17 +195,20 @@ class WalletContextualSuggestionsControllerTest : SysuiTestCase() {
        )
        )
    }
    }


    private fun verifyRegistered() {
    private fun verifyBroadcastReceiverRegistered() {
        verify(broadcastDispatcher)
        verify(broadcastDispatcher)
            .registerReceiver(capture(broadcastReceiver), any(), isNull(), isNull(), any(), any())
            .registerReceiver(
                capture(broadcastReceiver),
                any(),
                nullable(),
                nullable(),
                anyInt(),
                nullable()
            )
    }
    }


    private fun createContextualCardsIntent(
    private fun turnScreenOn() {
        ids: List<String> = emptyList(),
        broadcastReceiver.value.onReceive(mockContext, Intent(Intent.ACTION_SCREEN_ON))
    ): Intent {
        val intent = Intent(INTENT_NAME)
        intent.putStringArrayListExtra("cardIds", ArrayList(ids))
        return intent
    }
    }


    private fun setUpWalletClient(
    private fun setUpWalletClient(
@@ -238,6 +235,7 @@ class WalletContextualSuggestionsControllerTest : SysuiTestCase() {
        private val CARD_2: WalletCard = mock()
        private val CARD_2: WalletCard = mock()
        private const val ID_3: String = "789"
        private const val ID_3: String = "789"
        private val CARD_3: WalletCard = mock()
        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()
    }
    }
}
}