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