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