1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.wallet.controller 18 19 import android.content.Intent 20 import android.content.IntentFilter 21 import android.service.quickaccesswallet.GetWalletCardsError 22 import android.service.quickaccesswallet.GetWalletCardsResponse 23 import android.service.quickaccesswallet.QuickAccessWalletClient 24 import android.service.quickaccesswallet.WalletCard 25 import com.android.systemui.broadcast.BroadcastDispatcher 26 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 27 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.dagger.qualifiers.Application 30 import com.android.systemui.flags.FeatureFlags 31 import com.android.systemui.flags.Flags 32 import javax.inject.Inject 33 import kotlinx.coroutines.CoroutineScope 34 import kotlinx.coroutines.ExperimentalCoroutinesApi 35 import kotlinx.coroutines.channels.awaitClose 36 import kotlinx.coroutines.flow.Flow 37 import kotlinx.coroutines.flow.MutableStateFlow 38 import kotlinx.coroutines.flow.SharingStarted 39 import kotlinx.coroutines.flow.StateFlow 40 import kotlinx.coroutines.flow.asStateFlow 41 import kotlinx.coroutines.flow.combine 42 import kotlinx.coroutines.flow.flatMapLatest 43 import kotlinx.coroutines.flow.onEach 44 import kotlinx.coroutines.flow.stateIn 45 import kotlinx.coroutines.flow.update 46 import kotlinx.coroutines.launch 47 48 @OptIn(ExperimentalCoroutinesApi::class) 49 @SysUISingleton 50 class WalletContextualSuggestionsController 51 @Inject 52 constructor( 53 @Application private val applicationCoroutineScope: CoroutineScope, 54 private val walletController: QuickAccessWalletController, 55 broadcastDispatcher: BroadcastDispatcher, 56 featureFlags: FeatureFlags 57 ) { 58 private val cardsReceivedCallbacks: MutableSet<(List<WalletCard>) -> Unit> = mutableSetOf() 59 60 /** All potential cards. */ 61 val allWalletCards: StateFlow<List<WalletCard>> = 62 if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { 63 // TODO(b/237409756) determine if we should debounce this so we don't call the service 64 // too frequently. Also check if the list actually changed before calling callbacks. 65 broadcastDispatcher 66 .broadcastFlow(IntentFilter(Intent.ACTION_SCREEN_ON)) 67 .flatMapLatest { 68 conflatedCallbackFlow { 69 val callback = 70 object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { 71 override fun onWalletCardsRetrieved( 72 response: GetWalletCardsResponse 73 ) { 74 trySendWithFailureLogging(response.walletCards, TAG) 75 } 76 77 override fun onWalletCardRetrievalError( 78 error: GetWalletCardsError 79 ) { 80 trySendWithFailureLogging(emptyList<WalletCard>(), TAG) 81 } 82 } 83 84 walletController.setupWalletChangeObservers( 85 callback, 86 QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, 87 QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE 88 ) 89 walletController.updateWalletPreference() 90 walletController.queryWalletCards(callback, MAX_CARDS) 91 92 awaitClose { 93 walletController.unregisterWalletChangeObservers( 94 QuickAccessWalletController.WalletChangeEvent 95 .WALLET_PREFERENCE_CHANGE, 96 QuickAccessWalletController.WalletChangeEvent 97 .DEFAULT_PAYMENT_APP_CHANGE 98 ) 99 } 100 } 101 } 102 .onEach { notifyCallbacks(it) } 103 .stateIn( 104 applicationCoroutineScope, 105 // Needs to be done eagerly since we need to notify callbacks even if there are 106 // no subscribers 107 SharingStarted.Eagerly, 108 emptyList() 109 ) 110 } else { 111 MutableStateFlow<List<WalletCard>>(emptyList()).asStateFlow() 112 } 113 114 private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet()) 115 private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow() 116 117 /** Contextually-relevant cards. */ 118 val contextualSuggestionCards: Flow<List<WalletCard>> = 119 combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids -> 120 val ret = 121 cards.filter { card -> 122 card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT && 123 ids.contains(card.cardId) 124 } 125 ret 126 } 127 .stateIn(applicationCoroutineScope, SharingStarted.WhileSubscribed(), emptyList()) 128 129 /** When called, {@link contextualSuggestionCards} will be updated to be for these IDs. */ 130 fun setSuggestionCardIds(cardIds: Set<String>) { 131 _suggestionCardIds.update { _ -> cardIds } 132 } 133 134 /** Register callback to be called when a new list of cards is fetched. */ 135 fun registerWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) { 136 cardsReceivedCallbacks.add(callback) 137 } 138 139 /** Unregister callback to be called when a new list of cards is fetched. */ 140 fun unregisterWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) { 141 cardsReceivedCallbacks.remove(callback) 142 } 143 144 private fun notifyCallbacks(cards: List<WalletCard>) { 145 applicationCoroutineScope.launch { 146 cardsReceivedCallbacks.onEach { callback -> 147 callback(cards.filter { card -> card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT }) 148 } 149 } 150 } 151 152 companion object { 153 private const val TAG = "WalletSuggestions" 154 private const val MAX_CARDS = 50 155 } 156 } 157