1 /* <lambda>null2 * 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.utils.coroutines.flow.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.channels.awaitClose 35 import kotlinx.coroutines.flow.Flow 36 import kotlinx.coroutines.flow.MutableStateFlow 37 import kotlinx.coroutines.flow.SharingStarted 38 import kotlinx.coroutines.flow.StateFlow 39 import kotlinx.coroutines.flow.asStateFlow 40 import kotlinx.coroutines.flow.combine 41 import kotlinx.coroutines.flow.flatMapLatest 42 import kotlinx.coroutines.flow.onEach 43 import kotlinx.coroutines.flow.stateIn 44 import kotlinx.coroutines.flow.update 45 import com.android.app.tracing.coroutines.launchTraced as launch 46 47 @SysUISingleton 48 class WalletContextualSuggestionsController 49 @Inject 50 constructor( 51 @Application private val applicationCoroutineScope: CoroutineScope, 52 private val walletController: QuickAccessWalletController, 53 broadcastDispatcher: BroadcastDispatcher, 54 featureFlags: FeatureFlags 55 ) { 56 private val cardsReceivedCallbacks: MutableSet<(List<WalletCard>) -> Unit> = mutableSetOf() 57 58 /** All potential cards. */ 59 val allWalletCards: StateFlow<List<WalletCard>> = 60 if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { 61 // TODO(b/237409756) determine if we should debounce this so we don't call the service 62 // too frequently. Also check if the list actually changed before calling callbacks. 63 broadcastDispatcher 64 .broadcastFlow(IntentFilter(Intent.ACTION_SCREEN_ON)) 65 .flatMapLatest { 66 conflatedCallbackFlow { 67 val callback = 68 object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { 69 override fun onWalletCardsRetrieved( 70 response: GetWalletCardsResponse 71 ) { 72 trySendWithFailureLogging(response.walletCards, TAG) 73 } 74 75 override fun onWalletCardRetrievalError( 76 error: GetWalletCardsError 77 ) { 78 trySendWithFailureLogging(emptyList<WalletCard>(), TAG) 79 } 80 } 81 82 walletController.setupWalletChangeObservers( 83 callback, 84 QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, 85 QuickAccessWalletController.WalletChangeEvent 86 .DEFAULT_PAYMENT_APP_CHANGE, 87 QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_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 QuickAccessWalletController.WalletChangeEvent 99 .DEFAULT_WALLET_APP_CHANGE 100 ) 101 } 102 } 103 } 104 .onEach { notifyCallbacks(it) } 105 .stateIn( 106 applicationCoroutineScope, 107 // Needs to be done eagerly since we need to notify callbacks even if there are 108 // no subscribers 109 SharingStarted.Eagerly, 110 emptyList() 111 ) 112 } else { 113 MutableStateFlow<List<WalletCard>>(emptyList()).asStateFlow() 114 } 115 116 private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet()) 117 private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow() 118 119 /** Contextually-relevant cards. */ 120 val contextualSuggestionCards: Flow<List<WalletCard>> = 121 combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids -> 122 val ret = 123 cards.filter { card -> 124 card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT && 125 ids.contains(card.cardId) 126 } 127 ret 128 } 129 .stateIn(applicationCoroutineScope, SharingStarted.WhileSubscribed(), emptyList()) 130 131 /** When called, {@link contextualSuggestionCards} will be updated to be for these IDs. */ 132 fun setSuggestionCardIds(cardIds: Set<String>) { 133 _suggestionCardIds.update { _ -> cardIds } 134 } 135 136 /** Register callback to be called when a new list of cards is fetched. */ 137 fun registerWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) { 138 cardsReceivedCallbacks.add(callback) 139 } 140 141 /** Unregister callback to be called when a new list of cards is fetched. */ 142 fun unregisterWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) { 143 cardsReceivedCallbacks.remove(callback) 144 } 145 146 private fun notifyCallbacks(cards: List<WalletCard>) { 147 applicationCoroutineScope.launch { 148 cardsReceivedCallbacks.onEach { callback -> 149 callback(cards.filter { card -> card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT }) 150 } 151 } 152 } 153 154 companion object { 155 private const val TAG = "WalletSuggestions" 156 private const val MAX_CARDS = 50 157 } 158 } 159