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.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 88 .DEFAULT_PAYMENT_APP_CHANGE, 89 QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE 90 ) 91 walletController.updateWalletPreference() 92 walletController.queryWalletCards(callback, MAX_CARDS) 93 94 awaitClose { 95 walletController.unregisterWalletChangeObservers( 96 QuickAccessWalletController.WalletChangeEvent 97 .WALLET_PREFERENCE_CHANGE, 98 QuickAccessWalletController.WalletChangeEvent 99 .DEFAULT_PAYMENT_APP_CHANGE, 100 QuickAccessWalletController.WalletChangeEvent 101 .DEFAULT_WALLET_APP_CHANGE 102 ) 103 } 104 } 105 } 106 .onEach { notifyCallbacks(it) } 107 .stateIn( 108 applicationCoroutineScope, 109 // Needs to be done eagerly since we need to notify callbacks even if there are 110 // no subscribers 111 SharingStarted.Eagerly, 112 emptyList() 113 ) 114 } else { 115 MutableStateFlow<List<WalletCard>>(emptyList()).asStateFlow() 116 } 117 118 private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet()) 119 private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow() 120 121 /** Contextually-relevant cards. */ 122 val contextualSuggestionCards: Flow<List<WalletCard>> = 123 combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids -> 124 val ret = 125 cards.filter { card -> 126 card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT && 127 ids.contains(card.cardId) 128 } 129 ret 130 } 131 .stateIn(applicationCoroutineScope, SharingStarted.WhileSubscribed(), emptyList()) 132 133 /** When called, {@link contextualSuggestionCards} will be updated to be for these IDs. */ 134 fun setSuggestionCardIds(cardIds: Set<String>) { 135 _suggestionCardIds.update { _ -> cardIds } 136 } 137 138 /** Register callback to be called when a new list of cards is fetched. */ 139 fun registerWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) { 140 cardsReceivedCallbacks.add(callback) 141 } 142 143 /** Unregister callback to be called when a new list of cards is fetched. */ 144 fun unregisterWalletCardsReceivedCallback(callback: (List<WalletCard>) -> Unit) { 145 cardsReceivedCallbacks.remove(callback) 146 } 147 148 private fun notifyCallbacks(cards: List<WalletCard>) { 149 applicationCoroutineScope.launch { 150 cardsReceivedCallbacks.onEach { callback -> 151 callback(cards.filter { card -> card.cardType == WalletCard.CARD_TYPE_NON_PAYMENT }) 152 } 153 } 154 } 155 156 companion object { 157 private const val TAG = "WalletSuggestions" 158 private const val MAX_CARDS = 50 159 } 160 } 161