1 /*
<lambda>null2  * Copyright (C) 2022 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 
18 package com.android.systemui.keyguard.data.quickaffordance
19 
20 import android.content.Context
21 import android.graphics.drawable.Drawable
22 import android.service.quickaccesswallet.GetWalletCardsError
23 import android.service.quickaccesswallet.GetWalletCardsResponse
24 import android.service.quickaccesswallet.QuickAccessWalletClient
25 import android.service.quickaccesswallet.WalletCard
26 import android.util.Log
27 import com.android.systemui.animation.Expandable
28 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
29 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
30 import com.android.systemui.common.shared.model.ContentDescription
31 import com.android.systemui.common.shared.model.Icon
32 import com.android.systemui.dagger.SysUISingleton
33 import com.android.systemui.dagger.qualifiers.Application
34 import com.android.systemui.dagger.qualifiers.Background
35 import com.android.systemui.plugins.ActivityStarter
36 import com.android.systemui.res.R
37 import com.android.systemui.wallet.controller.QuickAccessWalletController
38 import com.android.systemui.wallet.util.getPaymentCards
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineDispatcher
41 import kotlinx.coroutines.ExperimentalCoroutinesApi
42 import kotlinx.coroutines.channels.awaitClose
43 import kotlinx.coroutines.flow.Flow
44 import kotlinx.coroutines.flow.flatMapLatest
45 import kotlinx.coroutines.flow.flowOf
46 import kotlinx.coroutines.suspendCancellableCoroutine
47 import kotlinx.coroutines.withContext
48 
49 /** Quick access wallet quick affordance data source. */
50 @SysUISingleton
51 class QuickAccessWalletKeyguardQuickAffordanceConfig
52 @Inject
53 constructor(
54     @Application private val context: Context,
55     @Background private val backgroundDispatcher: CoroutineDispatcher,
56     private val walletController: QuickAccessWalletController,
57     private val activityStarter: ActivityStarter,
58 ) : KeyguardQuickAffordanceConfig {
59 
60     override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
61 
62     override fun pickerName(): String = context.getString(R.string.accessibility_wallet_button)
63 
64     override val pickerIconResourceId = R.drawable.ic_wallet_lockscreen
65 
66     @OptIn(ExperimentalCoroutinesApi::class)
67     override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
68         conflatedCallbackFlow {
69                 val callback =
70                     object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
71                         override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
72                             val hasCards =
73                                 getPaymentCards(response.walletCards)?.isNotEmpty() == true
74                             trySendWithFailureLogging(
75                                 hasCards,
76                                 TAG,
77                             )
78                         }
79 
80                         override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
81                             Log.e(
82                                 TAG,
83                                 "Wallet card retrieval error, message: \"${error?.message}\""
84                             )
85                             trySendWithFailureLogging(
86                                 null,
87                                 TAG,
88                             )
89                         }
90                     }
91 
92                 walletController.setupWalletChangeObservers(
93                     callback,
94                     QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
95                     QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE,
96                     QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE
97                 )
98 
99                 withContext(backgroundDispatcher) {
100                     // Both must be called on background thread
101                     walletController.updateWalletPreference()
102                     walletController.queryWalletCards(callback)
103                 }
104 
105                 awaitClose {
106                     walletController.unregisterWalletChangeObservers(
107                         QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
108                         QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE,
109                         QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE
110                     )
111                 }
112             }
113             .flatMapLatest { hasCards ->
114                 // If hasCards is null, this indicates an error occurred upon card retrieval
115                 val state =
116                     if (hasCards == null) {
117                         KeyguardQuickAffordanceConfig.LockScreenState.Hidden
118                     } else {
119                         state(
120                             isWalletAvailable(),
121                             hasCards,
122                             walletController.walletClient.tileIcon,
123                         )
124                     }
125                 flowOf(state)
126             }
127 
128     override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
129         return when {
130             !walletController.walletClient.isWalletServiceAvailable ->
131                 KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
132             !isWalletAvailable() ->
133                 KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
134                     explanation =
135                         context.getString(
136                             R.string.wallet_quick_affordance_unavailable_install_the_app
137                         ),
138                 )
139             queryCards().isEmpty() ->
140                 KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
141                     explanation =
142                         context.getString(
143                             R.string.wallet_quick_affordance_unavailable_configure_the_app
144                         ),
145                 )
146             else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default()
147         }
148     }
149 
150     override fun onTriggered(
151         expandable: Expandable?,
152     ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
153         walletController.startQuickAccessUiIntent(
154             activityStarter,
155             expandable?.activityTransitionController(),
156             /* hasCard= */ true,
157         )
158         return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
159     }
160 
161     private suspend fun queryCards(): List<WalletCard> {
162         return withContext(backgroundDispatcher) {
163             suspendCancellableCoroutine { continuation ->
164                 val callback =
165                     object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
166                         override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
167                             continuation.resumeWith(
168                                 Result.success(getPaymentCards(response.walletCards))
169                             )
170                         }
171 
172                         override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
173                             continuation.resumeWith(Result.success(emptyList()))
174                         }
175                     }
176                 // Must be called on background thread
177                 walletController.queryWalletCards(callback)
178             }
179         }
180     }
181 
182     private suspend fun isWalletAvailable() =
183         withContext(backgroundDispatcher) {
184             with(walletController.walletClient) {
185                 // Must be called on background thread
186                 isWalletServiceAvailable && isWalletFeatureAvailable
187             }
188         }
189 
190     private fun state(
191         isFeatureEnabled: Boolean,
192         hasCard: Boolean,
193         tileIcon: Drawable?,
194     ): KeyguardQuickAffordanceConfig.LockScreenState {
195         return if (isFeatureEnabled && hasCard && tileIcon != null) {
196             KeyguardQuickAffordanceConfig.LockScreenState.Visible(
197                 icon =
198                     Icon.Loaded(
199                         drawable = tileIcon,
200                         contentDescription =
201                             ContentDescription.Resource(
202                                 res = R.string.accessibility_wallet_button,
203                             ),
204                     ),
205             )
206         } else {
207             KeyguardQuickAffordanceConfig.LockScreenState.Hidden
208         }
209     }
210 
211     companion object {
212         private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig"
213     }
214 }
215