Merge "Add WalletContextualSuggestionsController" into tm-qpr-dev am: 2a1702549e
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20948430 Change-Id: I75983e38262167b1c241837c6bb038ec975d14b0 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user