1 /*
2  * Copyright (C) 2021 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.qs.tiles;
18 
19 import static android.content.pm.PackageManager.FEATURE_NFC_HOST_CARD_EMULATION;
20 import static android.provider.Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT;
21 
22 import static com.google.common.truth.Truth.assertThat;
23 
24 import static junit.framework.TestCase.assertEquals;
25 import static junit.framework.TestCase.assertFalse;
26 import static junit.framework.TestCase.assertNotNull;
27 import static junit.framework.TestCase.assertNull;
28 import static junit.framework.TestCase.assertTrue;
29 
30 import static org.mockito.ArgumentMatchers.any;
31 import static org.mockito.ArgumentMatchers.eq;
32 import static org.mockito.Mockito.doNothing;
33 import static org.mockito.Mockito.spy;
34 import static org.mockito.Mockito.times;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.verifyZeroInteractions;
37 import static org.mockito.Mockito.when;
38 
39 import android.app.PendingIntent;
40 import android.content.ComponentName;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.pm.PackageManager;
44 import android.graphics.Bitmap;
45 import android.graphics.drawable.Drawable;
46 import android.graphics.drawable.Icon;
47 import android.os.Handler;
48 import android.os.UserHandle;
49 import android.service.quickaccesswallet.GetWalletCardsError;
50 import android.service.quickaccesswallet.GetWalletCardsResponse;
51 import android.service.quickaccesswallet.QuickAccessWalletClient;
52 import android.service.quickaccesswallet.QuickAccessWalletService;
53 import android.service.quickaccesswallet.WalletCard;
54 import android.service.quicksettings.Tile;
55 import android.testing.TestableLooper;
56 
57 import androidx.test.ext.junit.runners.AndroidJUnit4;
58 import androidx.test.filters.SmallTest;
59 
60 import com.android.internal.logging.MetricsLogger;
61 import com.android.systemui.SysuiTestCase;
62 import com.android.systemui.classifier.FalsingManagerFake;
63 import com.android.systemui.plugins.ActivityStarter;
64 import com.android.systemui.plugins.qs.QSTile;
65 import com.android.systemui.plugins.statusbar.StatusBarStateController;
66 import com.android.systemui.qs.QSHost;
67 import com.android.systemui.qs.QsEventLogger;
68 import com.android.systemui.qs.logging.QSLogger;
69 import com.android.systemui.qs.tileimpl.QSTileImpl;
70 import com.android.systemui.res.R;
71 import com.android.systemui.statusbar.policy.KeyguardStateController;
72 import com.android.systemui.util.settings.SecureSettings;
73 import com.android.systemui.wallet.controller.QuickAccessWalletController;
74 
75 import org.junit.After;
76 import org.junit.Before;
77 import org.junit.Test;
78 import org.junit.runner.RunWith;
79 import org.mockito.ArgumentCaptor;
80 import org.mockito.Captor;
81 import org.mockito.Mock;
82 import org.mockito.MockitoAnnotations;
83 
84 import java.util.Collections;
85 
86 @RunWith(AndroidJUnit4.class)
87 @TestableLooper.RunWithLooper(setAsMainLooper = true)
88 @SmallTest
89 public class QuickAccessWalletTileTest extends SysuiTestCase {
90 
91     private static final String CARD_ID = "card_id";
92     private static final String LABEL = "QAW";
93     private static final String CARD_DESCRIPTION = "•••• 1234";
94     private static final Icon CARD_IMAGE =
95             Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888));
96     private static final int PRIMARY_USER_ID = 0;
97     private static final int SECONDARY_USER_ID = 10;
98 
99     private final Drawable mTileIcon = mContext.getDrawable(R.drawable.ic_qs_wallet);
100     private final Intent mWalletIntent = new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET)
101             .setComponent(new ComponentName(mContext.getPackageName(), "WalletActivity"));
102 
103     @Mock
104     private QSHost mHost;
105     @Mock
106     private MetricsLogger mMetricsLogger;
107     @Mock
108     private StatusBarStateController mStatusBarStateController;
109     @Mock
110     private ActivityStarter mActivityStarter;
111     @Mock
112     private QSLogger mQSLogger;
113     @Mock
114     private QsEventLogger mUiEventLogger;
115     @Mock
116     private QuickAccessWalletClient mQuickAccessWalletClient;
117     @Mock
118     private KeyguardStateController mKeyguardStateController;
119     @Mock
120     private PackageManager mPackageManager;
121     @Mock
122     private SecureSettings mSecureSettings;
123     @Mock
124     private QuickAccessWalletController mController;
125     @Mock
126     private Icon mCardImage;
127     @Captor
128     ArgumentCaptor<QuickAccessWalletClient.OnWalletCardsRetrievedCallback> mCallbackCaptor;
129 
130     private Context mSpiedContext;
131     private TestableLooper mTestableLooper;
132     private QuickAccessWalletTile mTile;
133 
134     @Before
setUp()135     public void setUp() throws Exception {
136         MockitoAnnotations.initMocks(this);
137 
138         mTestableLooper = TestableLooper.get(this);
139         mSpiedContext = spy(mContext);
140 
141         doNothing().when(mSpiedContext).startActivity(any(Intent.class));
142         when(mHost.getContext()).thenReturn(mSpiedContext);
143         when(mQuickAccessWalletClient.getServiceLabel()).thenReturn(LABEL);
144         when(mQuickAccessWalletClient.getTileIcon()).thenReturn(mTileIcon);
145         when(mQuickAccessWalletClient.isWalletFeatureAvailable()).thenReturn(true);
146         when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(true);
147         when(mQuickAccessWalletClient.isWalletFeatureAvailableWhenDeviceLocked()).thenReturn(true);
148         when(mController.getWalletClient()).thenReturn(mQuickAccessWalletClient);
149         when(mCardImage.getType()).thenReturn(Icon.TYPE_URI);
150         when(mCardImage.loadDrawableAsUser(any(), eq(SECONDARY_USER_ID))).thenReturn(null);
151 
152         mTile = new QuickAccessWalletTile(
153                 mHost,
154                 mUiEventLogger,
155                 mTestableLooper.getLooper(),
156                 new Handler(mTestableLooper.getLooper()),
157                 new FalsingManagerFake(),
158                 mMetricsLogger,
159                 mStatusBarStateController,
160                 mActivityStarter,
161                 mQSLogger,
162                 mKeyguardStateController,
163                 mPackageManager,
164                 mSecureSettings,
165                 mController);
166 
167         mTile.initialize();
168         mTestableLooper.processAllMessages();
169     }
170 
171     @After
tearDown()172     public void tearDown() {
173         mTile.destroy();
174         mTestableLooper.processAllMessages();
175     }
176 
177     @Test
testNewTile()178     public void testNewTile() {
179         assertFalse(mTile.newTileState().handlesLongClick);
180     }
181 
182     @Test
testWalletServiceUnavailable_recreateWalletClient()183     public void testWalletServiceUnavailable_recreateWalletClient() {
184         when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(false);
185 
186         mTile.handleSetListening(true);
187 
188         verify(mController, times(1)).reCreateWalletClient();
189     }
190 
191     @Test
testWalletFeatureUnavailable_recreateWalletClient()192     public void testWalletFeatureUnavailable_recreateWalletClient() {
193         when(mQuickAccessWalletClient.isWalletFeatureAvailable()).thenReturn(false);
194 
195         mTile.handleSetListening(true);
196 
197         verify(mController, times(1)).reCreateWalletClient();
198     }
199 
200     @Test
testIsAvailable_qawFeatureAvailableWalletUnavailable()201     public void testIsAvailable_qawFeatureAvailableWalletUnavailable() {
202         when(mController.isWalletRoleAvailable()).thenReturn(false);
203         when(mPackageManager.hasSystemFeature(FEATURE_NFC_HOST_CARD_EMULATION)).thenReturn(true);
204         when(mPackageManager.hasSystemFeature("org.chromium.arc")).thenReturn(false);
205         when(mSecureSettings.getStringForUser(NFC_PAYMENT_DEFAULT_COMPONENT,
206                 UserHandle.USER_CURRENT)).thenReturn("Component");
207 
208         assertTrue(mTile.isAvailable());
209     }
210 
211     @Test
testIsAvailable_nfcUnavailableWalletAvailable()212     public void testIsAvailable_nfcUnavailableWalletAvailable() {
213         when(mController.isWalletRoleAvailable()).thenReturn(true);
214         when(mHost.getUserId()).thenReturn(PRIMARY_USER_ID);
215         when(mPackageManager.hasSystemFeature(FEATURE_NFC_HOST_CARD_EMULATION)).thenReturn(false);
216         when(mPackageManager.hasSystemFeature("org.chromium.arc")).thenReturn(false);
217 
218         assertTrue(mTile.isAvailable());
219     }
220 
221     @Test
testHandleClick_startQuickAccessUiIntent_noCard()222     public void testHandleClick_startQuickAccessUiIntent_noCard() {
223         setUpWalletCard(/* hasCard= */ false);
224 
225         mTile.handleClick(/* view= */ null);
226         mTestableLooper.processAllMessages();
227 
228         verify(mController).startQuickAccessUiIntent(
229                 eq(mActivityStarter),
230                 eq(null),
231                 /* hasCard= */ eq(false));
232     }
233 
234     @Test
testHandleClick_startQuickAccessUiIntent_hasCard()235     public void testHandleClick_startQuickAccessUiIntent_hasCard() {
236         setUpWalletCard(/* hasCard= */ true);
237 
238         mTile.handleClick(null /* view */);
239         mTestableLooper.processAllMessages();
240 
241         verify(mController).startQuickAccessUiIntent(
242                 eq(mActivityStarter),
243                 eq(null),
244                 /* hasCard= */ eq(true));
245     }
246 
247     @Test
testHandleUpdateState_updateLabelAndIcon()248     public void testHandleUpdateState_updateLabelAndIcon() {
249         QSTile.State state = new QSTile.State();
250 
251         mTile.handleUpdateState(state, null);
252 
253         assertEquals(LABEL, state.label.toString());
254         assertTrue(state.label.toString().contentEquals(state.contentDescription));
255         assertEquals(mTileIcon, state.icon.getDrawable(mContext));
256     }
257 
258     @Test
testHandleUpdateState_updateLabelAndIcon_noIconFromApi()259     public void testHandleUpdateState_updateLabelAndIcon_noIconFromApi() {
260         when(mQuickAccessWalletClient.getTileIcon()).thenReturn(null);
261         QSTile.State state = new QSTile.State();
262         QSTile.Icon icon = QSTileImpl.ResourceIcon.get(R.drawable.ic_wallet_lockscreen);
263 
264         mTile.handleUpdateState(state, null);
265 
266         assertEquals(LABEL, state.label.toString());
267         assertTrue(state.label.toString().contentEquals(state.contentDescription));
268         assertEquals(icon, state.icon);
269     }
270 
271     @Test
testGetTileLabel_serviceLabelExists()272     public void testGetTileLabel_serviceLabelExists() {
273         assertEquals(LABEL, mTile.getTileLabel().toString());
274     }
275 
276     @Test
testGetTileLabel_serviceLabelDoesNotExist()277     public void testGetTileLabel_serviceLabelDoesNotExist() {
278         when(mQuickAccessWalletClient.getServiceLabel()).thenReturn(null);
279         assertEquals(mContext.getString(R.string.wallet_title), mTile.getTileLabel().toString());
280     }
281 
282     @Test
testHandleUpdateState_walletIsUpdating()283     public void testHandleUpdateState_walletIsUpdating() {
284         when(mKeyguardStateController.isUnlocked()).thenReturn(true);
285         QSTile.State state = new QSTile.State();
286         GetWalletCardsResponse response =
287                 new GetWalletCardsResponse(
288                         Collections.singletonList(createWalletCard(mContext)), 0);
289 
290         mTile.handleSetListening(true);
291 
292         verify(mController).queryWalletCards(mCallbackCaptor.capture());
293 
294         // Wallet cards fetching on its way; wallet updating.
295         mTile.handleUpdateState(state, null);
296 
297         assertEquals(Tile.STATE_INACTIVE, state.state);
298         assertEquals(
299                 mContext.getString(R.string.wallet_secondary_label_updating), state.secondaryLabel);
300         assertNotNull(state.stateDescription);
301         assertNull(state.sideViewCustomDrawable);
302 
303         // Wallet cards fetching completed.
304         mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
305         mTestableLooper.processAllMessages();
306 
307         mTile.handleUpdateState(state, null);
308 
309         assertEquals(Tile.STATE_ACTIVE, state.state);
310         assertEquals(CARD_DESCRIPTION, state.secondaryLabel);
311         assertNotNull(state.stateDescription);
312         assertNotNull(state.sideViewCustomDrawable);
313     }
314 
315     @Test
testHandleUpdateState_hasCard_deviceLocked_tileInactive()316     public void testHandleUpdateState_hasCard_deviceLocked_tileInactive() {
317         when(mKeyguardStateController.isUnlocked()).thenReturn(false);
318         QSTile.State state = new QSTile.State();
319         setUpWalletCard(/* hasCard= */ true);
320 
321         mTile.handleUpdateState(state, null);
322 
323         assertEquals(Tile.STATE_INACTIVE, state.state);
324         assertEquals(CARD_DESCRIPTION, state.secondaryLabel);
325         assertNotNull(state.stateDescription);
326         assertNotNull(state.sideViewCustomDrawable);
327     }
328 
329     @Test
testHandleUpdateState_hasCard_deviceUnlocked_tileActive()330     public void testHandleUpdateState_hasCard_deviceUnlocked_tileActive() {
331         when(mKeyguardStateController.isUnlocked()).thenReturn(true);
332         QSTile.State state = new QSTile.State();
333         setUpWalletCard(/* hasCard= */ true);
334 
335         mTile.handleUpdateState(state, null);
336 
337         assertEquals(Tile.STATE_ACTIVE, state.state);
338         assertEquals(CARD_DESCRIPTION, state.secondaryLabel);
339         assertNotNull(state.stateDescription);
340         assertNotNull(state.sideViewCustomDrawable);
341     }
342 
343 
344     @Test
testHandleUpdateState_noCard_tileInactive()345     public void testHandleUpdateState_noCard_tileInactive() {
346         QSTile.State state = new QSTile.State();
347         setUpWalletCard(/* hasCard= */ false);
348 
349         mTile.handleUpdateState(state, null);
350 
351         assertEquals(Tile.STATE_INACTIVE, state.state);
352         assertEquals(
353                 mContext.getString(R.string.wallet_secondary_label_no_card),
354                 state.secondaryLabel);
355         assertNotNull(state.stateDescription);
356         assertNull(state.sideViewCustomDrawable);
357     }
358 
359     @Test
testHandleUpdateState_qawServiceUnavailable_tileUnavailable()360     public void testHandleUpdateState_qawServiceUnavailable_tileUnavailable() {
361         when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(false);
362         QSTile.State state = new QSTile.State();
363 
364         mTile.handleUpdateState(state, null);
365 
366         assertEquals(Tile.STATE_UNAVAILABLE, state.state);
367         assertNull(state.stateDescription);
368         assertNull(state.sideViewCustomDrawable);
369     }
370 
371     @Test
testHandleUpdateState_qawFeatureUnavailable_tileUnavailable()372     public void testHandleUpdateState_qawFeatureUnavailable_tileUnavailable() {
373         when(mQuickAccessWalletClient.isWalletFeatureAvailable()).thenReturn(false);
374         QSTile.State state = new QSTile.State();
375 
376         mTile.handleUpdateState(state, null);
377 
378         assertEquals(Tile.STATE_UNAVAILABLE, state.state);
379         assertNull(state.stateDescription);
380         assertNull(state.sideViewCustomDrawable);
381     }
382 
383     @Test
testHandleSetListening_queryCards()384     public void testHandleSetListening_queryCards() {
385         mTile.handleSetListening(true);
386 
387         verify(mController).queryWalletCards(mCallbackCaptor.capture());
388 
389         assertThat(mCallbackCaptor.getValue()).isInstanceOf(
390                 QuickAccessWalletClient.OnWalletCardsRetrievedCallback.class);
391     }
392 
393     @Test
testQueryCards_hasCards_updateSideViewDrawable()394     public void testQueryCards_hasCards_updateSideViewDrawable() {
395         when(mKeyguardStateController.isUnlocked()).thenReturn(true);
396         setUpWalletCard(/* hasCard= */ true);
397 
398         assertNotNull(mTile.getState().sideViewCustomDrawable);
399     }
400 
401     @Test
testQueryCards_notCurrentUser_hasCards_noSideViewDrawable()402     public void testQueryCards_notCurrentUser_hasCards_noSideViewDrawable() {
403         when(mKeyguardStateController.isUnlocked()).thenReturn(true);
404 
405         PendingIntent pendingIntent =
406                 PendingIntent.getActivity(mContext, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
407         WalletCard walletCard =
408                 new WalletCard.Builder(
409                         CARD_ID, mCardImage, CARD_DESCRIPTION, pendingIntent).build();
410         GetWalletCardsResponse response =
411                 new GetWalletCardsResponse(Collections.singletonList(walletCard), 0);
412 
413         mTile.handleSetListening(true);
414 
415         verify(mController).queryWalletCards(mCallbackCaptor.capture());
416 
417         mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
418         mTestableLooper.processAllMessages();
419 
420         assertNull(mTile.getState().sideViewCustomDrawable);
421     }
422 
423     @Test
testQueryCards_cardDataPayment_updateSideViewDrawable()424     public void testQueryCards_cardDataPayment_updateSideViewDrawable() {
425         when(mKeyguardStateController.isUnlocked()).thenReturn(true);
426         setUpWalletCardWithType(/* hasCard =*/ true, WalletCard.CARD_TYPE_PAYMENT);
427 
428         assertNotNull(mTile.getState().sideViewCustomDrawable);
429     }
430 
431     @Test
testQueryCards_cardDataNonPayment_updateSideViewDrawable()432     public void testQueryCards_cardDataNonPayment_updateSideViewDrawable() {
433         when(mKeyguardStateController.isUnlocked()).thenReturn(true);
434         setUpWalletCardWithType(/* hasCard =*/ true, WalletCard.CARD_TYPE_NON_PAYMENT);
435 
436         assertNull(mTile.getState().sideViewCustomDrawable);
437     }
438 
439     @Test
testQueryCards_noCards_notUpdateSideViewDrawable()440     public void testQueryCards_noCards_notUpdateSideViewDrawable() {
441         setUpWalletCard(/* hasCard= */ false);
442 
443         assertNull(mTile.getState().sideViewCustomDrawable);
444     }
445 
446     @Test
testQueryCards_error_notUpdateSideViewDrawable()447     public void testQueryCards_error_notUpdateSideViewDrawable() {
448         String errorMessage = "getWalletCardsError";
449         GetWalletCardsError error = new GetWalletCardsError(CARD_IMAGE, errorMessage);
450 
451         mTile.handleSetListening(true);
452 
453         verify(mController).queryWalletCards(mCallbackCaptor.capture());
454 
455         mCallbackCaptor.getValue().onWalletCardRetrievalError(error);
456         mTestableLooper.processAllMessages();
457 
458         assertNull(mTile.getState().sideViewCustomDrawable);
459     }
460 
461     @Test
testHandleSetListening_notListening_notQueryCards()462     public void testHandleSetListening_notListening_notQueryCards() {
463         mTile.handleSetListening(false);
464 
465         verifyZeroInteractions(mQuickAccessWalletClient);
466     }
467 
createWalletCardWithType(Context context, int cardType)468     private WalletCard createWalletCardWithType(Context context, int cardType) {
469         PendingIntent pendingIntent =
470                 PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
471         return new WalletCard.Builder(CARD_ID, cardType, CARD_IMAGE, CARD_DESCRIPTION,
472                 pendingIntent).build();
473     }
474 
setUpWalletCardWithType(boolean hasCard, int cardType)475     private void setUpWalletCardWithType(boolean hasCard, int cardType) {
476         GetWalletCardsResponse response =
477                 new GetWalletCardsResponse(
478                         hasCard
479                                 ? Collections.singletonList(
480                                 createWalletCardWithType(mContext, cardType))
481                                 : Collections.EMPTY_LIST, 0);
482 
483         mTile.handleSetListening(true);
484 
485         verify(mController).queryWalletCards(mCallbackCaptor.capture());
486 
487         mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
488         mTestableLooper.processAllMessages();
489     }
490 
setUpWalletCard(boolean hasCard)491     private void setUpWalletCard(boolean hasCard) {
492         GetWalletCardsResponse response =
493                 new GetWalletCardsResponse(
494                         hasCard
495                                 ? Collections.singletonList(createWalletCard(mContext))
496                                 : Collections.EMPTY_LIST, 0);
497 
498         mTile.handleSetListening(true);
499 
500         verify(mController).queryWalletCards(mCallbackCaptor.capture());
501 
502         mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
503         mTestableLooper.processAllMessages();
504     }
505 
createWalletCard(Context context)506     private WalletCard createWalletCard(Context context) {
507         PendingIntent pendingIntent =
508                 PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
509         return new WalletCard.Builder(CARD_ID, CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build();
510     }
511 }
512