Loading packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.wallet.controller import android.Manifest import android.content.Context import android.content.IntentFilter import android.service.quickaccesswallet.GetWalletCardsError import android.service.quickaccesswallet.GetWalletCardsResponse import android.service.quickaccesswallet.QuickAccessWalletClient import android.service.quickaccesswallet.WalletCard import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.shareIn @SysUISingleton class WalletContextualSuggestionsController @Inject constructor( @Application private val applicationCoroutineScope: CoroutineScope, private val walletController: QuickAccessWalletController, broadcastDispatcher: BroadcastDispatcher, featureFlags: FeatureFlags ) { private val allWalletCards: Flow<List<WalletCard>> = if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { conflatedCallbackFlow { val callback = object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) { trySendWithFailureLogging(response.walletCards, TAG) } override fun onWalletCardRetrievalError(error: GetWalletCardsError) { trySendWithFailureLogging(emptyList<WalletCard>(), TAG) } } walletController.setupWalletChangeObservers( callback, QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE ) walletController.updateWalletPreference() walletController.queryWalletCards(callback) awaitClose { walletController.unregisterWalletChangeObservers( QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE ) } } } else { emptyFlow() } private val contextualSuggestionsCardIds: Flow<Set<String>> = if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { broadcastDispatcher.broadcastFlow( filter = IntentFilter(ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS), permission = Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE, flags = Context.RECEIVER_EXPORTED ) { intent, _ -> if (intent.hasExtra(UPDATE_CARD_IDS_EXTRA)) { intent.getStringArrayListExtra(UPDATE_CARD_IDS_EXTRA).toSet() } else { emptySet() } } } else { emptyFlow() } val contextualSuggestionCards: Flow<List<WalletCard>> = combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids -> cards.filter { card -> ids.contains(card.cardId) } } .shareIn(applicationCoroutineScope, replay = 1, started = SharingStarted.Eagerly) companion object { private const val ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS = "com.android.systemui.wallet.UPDATE_CONTEXTUAL_SUGGESTIONS" private const val UPDATE_CARD_IDS_EXTRA = "cardIds" private const val TAG = "WalletSuggestions" } } packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt 0 → 100644 +243 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.wallet.controller import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.service.quickaccesswallet.GetWalletCardsResponse import android.service.quickaccesswallet.QuickAccessWalletClient import android.service.quickaccesswallet.WalletCard import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.ArrayList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi 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.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.isNull import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class WalletContextualSuggestionsControllerTest : SysuiTestCase() { @Mock private lateinit var walletController: QuickAccessWalletController @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var mockContext: Context @Captor private lateinit var broadcastReceiver: ArgumentCaptor<BroadcastReceiver> @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever( broadcastDispatcher.broadcastFlow<List<String>?>( any(), isNull(), any(), any(), any() ) ) .thenCallRealMethod() whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS))) .thenReturn(true) whenever(CARD_1.cardId).thenReturn(ID_1) whenever(CARD_2.cardId).thenReturn(ID_2) whenever(CARD_3.cardId).thenReturn(ID_3) } @Test fun `state - has wallet cards - received contextual cards`() = runTest { setUpWalletClient(listOf(CARD_1, CARD_2)) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive( mockContext, createContextualCardsIntent(listOf(ID_1, ID_2)) ) assertThat(latest()).containsExactly(CARD_1, CARD_2) } @Test fun `state - no wallet cards - received contextual cards`() = runTest { setUpWalletClient(emptyList()) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive( mockContext, createContextualCardsIntent(listOf(ID_1, ID_2)) ) assertThat(latest()).isEmpty() } @Test fun `state - has wallet cards - no contextual cards`() = runTest { setUpWalletClient(listOf(CARD_1, CARD_2)) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive(mockContext, createContextualCardsIntent(emptyList())) assertThat(latest()).isEmpty() } @Test fun `state - wallet cards error`() = runTest { setUpWalletClient(shouldFail = true) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive( 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() } @Test fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest { whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS))) .thenReturn(false) setUpWalletClient(listOf(CARD_1, CARD_2)) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verify(broadcastDispatcher, never()).broadcastFlow(any(), isNull(), any(), any()) assertThat(latest()).isNull() } private fun createWalletContextualSuggestionsController( scope: CoroutineScope ): WalletContextualSuggestionsController { return WalletContextualSuggestionsController( scope, walletController, broadcastDispatcher, featureFlags ) } private fun verifyRegistered() { verify(broadcastDispatcher) .registerReceiver(capture(broadcastReceiver), any(), isNull(), isNull(), any(), any()) } private fun createContextualCardsIntent( ids: List<String> = emptyList(), ): Intent { val intent = Intent(INTENT_NAME) intent.putStringArrayListExtra("cardIds", ArrayList(ids)) return intent } private fun setUpWalletClient( cards: List<WalletCard> = emptyList(), shouldFail: Boolean = false ) { whenever(walletController.queryWalletCards(any())).thenAnswer { invocation -> with( invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback ) { if (shouldFail) { onWalletCardRetrievalError(mock()) } else { onWalletCardsRetrieved(GetWalletCardsResponse(cards, 0)) } } } } companion object { private const val ID_1: String = "123" private val CARD_1: WalletCard = mock() private const val ID_2: String = "456" private val CARD_2: WalletCard = mock() private const val ID_3: String = "789" private val CARD_3: WalletCard = mock() private val INTENT_NAME: String = "WalletSuggestionsIntent" } } Loading
packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.wallet.controller import android.Manifest import android.content.Context import android.content.IntentFilter import android.service.quickaccesswallet.GetWalletCardsError import android.service.quickaccesswallet.GetWalletCardsResponse import android.service.quickaccesswallet.QuickAccessWalletClient import android.service.quickaccesswallet.WalletCard import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.shareIn @SysUISingleton class WalletContextualSuggestionsController @Inject constructor( @Application private val applicationCoroutineScope: CoroutineScope, private val walletController: QuickAccessWalletController, broadcastDispatcher: BroadcastDispatcher, featureFlags: FeatureFlags ) { private val allWalletCards: Flow<List<WalletCard>> = if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { conflatedCallbackFlow { val callback = object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) { trySendWithFailureLogging(response.walletCards, TAG) } override fun onWalletCardRetrievalError(error: GetWalletCardsError) { trySendWithFailureLogging(emptyList<WalletCard>(), TAG) } } walletController.setupWalletChangeObservers( callback, QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE ) walletController.updateWalletPreference() walletController.queryWalletCards(callback) awaitClose { walletController.unregisterWalletChangeObservers( QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE ) } } } else { emptyFlow() } private val contextualSuggestionsCardIds: Flow<Set<String>> = if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { broadcastDispatcher.broadcastFlow( filter = IntentFilter(ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS), permission = Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE, flags = Context.RECEIVER_EXPORTED ) { intent, _ -> if (intent.hasExtra(UPDATE_CARD_IDS_EXTRA)) { intent.getStringArrayListExtra(UPDATE_CARD_IDS_EXTRA).toSet() } else { emptySet() } } } else { emptyFlow() } val contextualSuggestionCards: Flow<List<WalletCard>> = combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids -> cards.filter { card -> ids.contains(card.cardId) } } .shareIn(applicationCoroutineScope, replay = 1, started = SharingStarted.Eagerly) companion object { private const val ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS = "com.android.systemui.wallet.UPDATE_CONTEXTUAL_SUGGESTIONS" private const val UPDATE_CARD_IDS_EXTRA = "cardIds" private const val TAG = "WalletSuggestions" } }
packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt 0 → 100644 +243 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.wallet.controller import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.service.quickaccesswallet.GetWalletCardsResponse import android.service.quickaccesswallet.QuickAccessWalletClient import android.service.quickaccesswallet.WalletCard import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.ArrayList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi 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.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.isNull import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class WalletContextualSuggestionsControllerTest : SysuiTestCase() { @Mock private lateinit var walletController: QuickAccessWalletController @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var mockContext: Context @Captor private lateinit var broadcastReceiver: ArgumentCaptor<BroadcastReceiver> @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever( broadcastDispatcher.broadcastFlow<List<String>?>( any(), isNull(), any(), any(), any() ) ) .thenCallRealMethod() whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS))) .thenReturn(true) whenever(CARD_1.cardId).thenReturn(ID_1) whenever(CARD_2.cardId).thenReturn(ID_2) whenever(CARD_3.cardId).thenReturn(ID_3) } @Test fun `state - has wallet cards - received contextual cards`() = runTest { setUpWalletClient(listOf(CARD_1, CARD_2)) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive( mockContext, createContextualCardsIntent(listOf(ID_1, ID_2)) ) assertThat(latest()).containsExactly(CARD_1, CARD_2) } @Test fun `state - no wallet cards - received contextual cards`() = runTest { setUpWalletClient(emptyList()) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive( mockContext, createContextualCardsIntent(listOf(ID_1, ID_2)) ) assertThat(latest()).isEmpty() } @Test fun `state - has wallet cards - no contextual cards`() = runTest { setUpWalletClient(listOf(CARD_1, CARD_2)) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive(mockContext, createContextualCardsIntent(emptyList())) assertThat(latest()).isEmpty() } @Test fun `state - wallet cards error`() = runTest { setUpWalletClient(shouldFail = true) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verifyRegistered() broadcastReceiver.value.onReceive( 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() } @Test fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest { whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS))) .thenReturn(false) setUpWalletClient(listOf(CARD_1, CARD_2)) val latest = collectLastValue( createWalletContextualSuggestionsController(backgroundScope) .contextualSuggestionCards, ) runCurrent() verify(broadcastDispatcher, never()).broadcastFlow(any(), isNull(), any(), any()) assertThat(latest()).isNull() } private fun createWalletContextualSuggestionsController( scope: CoroutineScope ): WalletContextualSuggestionsController { return WalletContextualSuggestionsController( scope, walletController, broadcastDispatcher, featureFlags ) } private fun verifyRegistered() { verify(broadcastDispatcher) .registerReceiver(capture(broadcastReceiver), any(), isNull(), isNull(), any(), any()) } private fun createContextualCardsIntent( ids: List<String> = emptyList(), ): Intent { val intent = Intent(INTENT_NAME) intent.putStringArrayListExtra("cardIds", ArrayList(ids)) return intent } private fun setUpWalletClient( cards: List<WalletCard> = emptyList(), shouldFail: Boolean = false ) { whenever(walletController.queryWalletCards(any())).thenAnswer { invocation -> with( invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback ) { if (shouldFail) { onWalletCardRetrievalError(mock()) } else { onWalletCardsRetrieved(GetWalletCardsResponse(cards, 0)) } } } } companion object { private const val ID_1: String = "123" private val CARD_1: WalletCard = mock() private const val ID_2: String = "456" private val CARD_2: WalletCard = mock() private const val ID_3: String = "789" private val CARD_3: WalletCard = mock() private val INTENT_NAME: String = "WalletSuggestionsIntent" } }