• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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