1 /*
2  * Copyright (C) 2024 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.nfc.cardemulation;
18 
19 import static android.app.Activity.RESULT_CANCELED;
20 import static android.nfc.cardemulation.CardEmulation.CATEGORY_PAYMENT;
21 import static android.view.WindowManager.LayoutParams.FLAG_BLUR_BEHIND;
22 import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
23 import static androidx.test.espresso.Espresso.onView;
24 import static androidx.test.espresso.action.ViewActions.click;
25 import static androidx.test.espresso.assertion.ViewAssertions.matches;
26 import static androidx.test.espresso.matcher.ViewMatchers.withId;
27 import static androidx.test.espresso.matcher.ViewMatchers.withText;
28 import static com.android.nfc.cardemulation.AppChooserActivity.EXTRA_CATEGORY;
29 import static com.android.nfc.cardemulation.AppChooserActivity.EXTRA_APDU_SERVICES;
30 import static com.android.nfc.cardemulation.AppChooserActivity.EXTRA_FAILED_COMPONENT;
31 import static com.google.common.truth.Truth.assertThat;
32 
33 import android.content.ComponentName;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.pm.ApplicationInfo;
37 import android.content.pm.PackageManager;
38 import android.content.pm.ResolveInfo;
39 import android.content.pm.ServiceInfo;
40 import android.nfc.cardemulation.ApduServiceInfo;
41 import android.widget.ListView;
42 import android.view.Window;
43 import android.view.View;
44 
45 import androidx.lifecycle.Lifecycle;
46 import androidx.test.core.app.ActivityScenario;
47 import androidx.test.espresso.UiController;
48 import androidx.test.espresso.ViewAction;
49 import androidx.test.espresso.matcher.ViewMatchers;
50 import androidx.test.ext.junit.runners.AndroidJUnit4;
51 import androidx.test.platform.app.InstrumentationRegistry;
52 
53 import com.android.nfc.R;
54 
55 import java.lang.IllegalStateException;
56 import java.util.ArrayList;
57 import java.util.concurrent.CountDownLatch;
58 import java.util.concurrent.TimeUnit;
59 
60 import org.hamcrest.Matcher;
61 
62 import org.junit.Rule;
63 import org.junit.Before;
64 import org.junit.Test;
65 import org.junit.runner.RunWith;
66 
67 @RunWith(AndroidJUnit4.class)
68 public class AppChooserActivityTest {
69   private static final String UNKNOWN_LABEL = "unknown";
70   private Context context;
71 
72   @Before
setUp()73   public void setUp() throws Exception {
74     context = InstrumentationRegistry.getInstrumentation().getTargetContext();
75   }
76 
77   @Test
testNoFailedComponentAndNoAlternatives()78   public void testNoFailedComponentAndNoAlternatives() throws Exception {
79     ActivityScenario<AppChooserActivity> scenario
80         = ActivityScenario.launch(getIntent(/*isPayment = */ true,
81                                             /* withFailedComponent = */ false,
82                                             /* withServices = */ false));
83 
84     assertThat(scenario.getState()).isEqualTo(Lifecycle.State.DESTROYED);
85   }
86 
87   @Test
testExistingFailedComponentAndNoAlternatives()88   public void testExistingFailedComponentAndNoAlternatives() throws Exception {
89     ActivityScenario<AppChooserActivity> scenario
90         = ActivityScenario.launch(getIntent(/*isPayment = */ true,
91                                             /* withFailedComponent = */ true,
92                                             /* withServices = */ false));
93 
94     assertThat(scenario.getState()).isAtLeast(Lifecycle.State.CREATED);
95     String expectedText
96         = String.format(context.getString(R.string.transaction_failure), UNKNOWN_LABEL);
97     onView(withId(R.id.appchooser_text)).check(matches(withText(expectedText)));
98     scenario.onActivity(activity -> {
99       int flags = activity.getWindow().getAttributes().flags;
100       assertThat(flags & FLAG_BLUR_BEHIND).isEqualTo(FLAG_BLUR_BEHIND);
101       assertThat(flags & FLAG_DISMISS_KEYGUARD).isEqualTo(FLAG_DISMISS_KEYGUARD);
102     });
103   }
104 
105   @Test
testNonPayment()106   public void testNonPayment() throws Exception {
107     ActivityScenario<AppChooserActivity> scenario
108         = ActivityScenario.launch(getIntent(/*isPayment = */ false,
109                                             /* withFailedComponent = */ true,
110                                             /* withServices = */ true));
111 
112     scenario.onActivity(activity -> {
113       ListView listView = (ListView) activity.findViewById(R.id.resolver_list);
114       assertThat(listView.getDividerHeight()).isEqualTo(-1);
115       assertThat(listView.getPaddingEnd()).isEqualTo(0);
116       assertThat(listView.getPaddingLeft()).isEqualTo(0);
117       assertThat(listView.getPaddingRight()).isEqualTo(0);
118       assertThat(listView.getPaddingStart()).isEqualTo(0);
119     });
120   }
121 
122   @Test
testExistingFailedComponentAndExistingAlternatives()123   public void testExistingFailedComponentAndExistingAlternatives() throws Exception {
124     ActivityScenario<AppChooserActivity> scenario
125         = ActivityScenario.launch(getIntent(/*isPayment = */ true,
126                                                     /* withFailedComponent = */ true,
127                                                     /* withServices = */ true));
128 
129     assertThat(scenario.getState()).isAtLeast(Lifecycle.State.CREATED);
130     String expectedText
131         = String.format(context.getString(R.string.could_not_use_app), UNKNOWN_LABEL);
132     onView(withId(R.id.appchooser_text)).check(matches(withText(expectedText)));
133     scenario.onActivity(activity -> {
134       int flags = activity.getWindow().getAttributes().flags;
135       assertThat(flags & FLAG_BLUR_BEHIND).isEqualTo(FLAG_BLUR_BEHIND);
136       assertThat(flags & FLAG_DISMISS_KEYGUARD).isEqualTo(FLAG_DISMISS_KEYGUARD);
137 
138       ListView listView = (ListView) activity.findViewById(R.id.resolver_list);
139       assertThat(listView.getDivider()).isNotNull();
140       assertThat((int) listView.getDividerHeight())
141           .isEqualTo((int) (context.getResources().getDisplayMetrics().density * 16));
142       assertThat(listView.getAdapter()).isNotNull();
143     });
144 
145     // Test that onItemClick() does not throw an Exception
146     onView(withId(R.id.resolver_list)).perform(customClick());
147   }
148 
149   @Test
testNoFailedComponentAndExistingAlternatives()150   public void testNoFailedComponentAndExistingAlternatives() throws Exception {
151     ActivityScenario<AppChooserActivity> scenario
152         = ActivityScenario.launch(getIntent(/*isPayment = */ true,
153                                                     /* withFailedComponent = */ false,
154                                                     /* withServices = */ true));
155 
156     assertThat(scenario.getState()).isAtLeast(Lifecycle.State.CREATED);
157     String expectedText = context.getString(R.string.appchooser_description);
158     onView(withId(R.id.appchooser_text)).check(matches(withText(expectedText)));
159     scenario.onActivity(activity -> {
160       int flags = activity.getWindow().getAttributes().flags;
161       assertThat(flags & FLAG_BLUR_BEHIND).isEqualTo(FLAG_BLUR_BEHIND);
162       assertThat(flags & FLAG_DISMISS_KEYGUARD).isEqualTo(FLAG_DISMISS_KEYGUARD);
163 
164       ListView listView = (ListView) activity.findViewById(R.id.resolver_list);
165       assertThat(listView.getDivider()).isNotNull();
166       assertThat((int) listView.getDividerHeight())
167           .isEqualTo((int) (context.getResources().getDisplayMetrics().density * 16));
168       assertThat(listView.getAdapter()).isNotNull();
169     });
170 
171     // Test that onItemClick() does not throw an Exception
172     onView(withId(R.id.resolver_list)).perform(customClick());
173   }
174 
getIntent(boolean isPayment, boolean withFailedComponent, boolean withServices)175   private Intent getIntent(boolean isPayment, boolean withFailedComponent, boolean withServices) {
176     Intent intent = new Intent(context, AppChooserActivity.class);
177     if (isPayment) {
178       intent.putExtra(EXTRA_CATEGORY, CATEGORY_PAYMENT);
179     } else {
180       intent.putExtra(EXTRA_CATEGORY, "");
181     }
182 
183     ArrayList<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
184     if (withServices) {
185       ServiceInfo serviceInfo = new ServiceInfo();
186       serviceInfo.packageName = "com.nfc.test";
187       serviceInfo.name = "hce_service";
188       serviceInfo.applicationInfo = new ApplicationInfo();
189       ResolveInfo resolveInfo = new ResolveInfo();
190       resolveInfo.serviceInfo = serviceInfo;
191       ApduServiceInfo service
192           = new ApduServiceInfo(resolveInfo,
193                                 /* onHost = */ false,
194                                 /* description = */ "",
195                                 /* staticAidGroups = */ new ArrayList<>(),
196                                 /* dynamicAidGroups = */ new ArrayList<>(),
197                                 /* requiresUnlock = */ false,
198                                 /* bannerResource = */ 0,
199                                 /* uid = */ 0,
200                                 /* settingsActivityName = */ "",
201                                 /* offHost = */ "",
202                                 /* staticOffHost = */ "");
203       services.add(service);
204     }
205     intent.putParcelableArrayListExtra(EXTRA_APDU_SERVICES, services);
206 
207     if (withFailedComponent) {
208       ComponentName failedComponent
209           = new ComponentName("com.android.test.walletroleholder",
210                               "com.android.test.walletroleholder.WalletRoleHolderApduService");
211       intent.putExtra(EXTRA_FAILED_COMPONENT, failedComponent);
212     }
213     return intent;
214   }
215 
216   // Bypasses the view.getGlobalVisibleRect() requirement on the default click() action
customClick()217   private ViewAction customClick() {
218     return new ViewAction() {
219       @Override
220       public Matcher<View> getConstraints() {
221         return ViewMatchers.isEnabled();
222       }
223 
224       @Override
225       public String getDescription() {
226         return "";
227       }
228 
229       @Override
230       public void perform(UiController uiController, View view) {
231         view.performClick();
232       }
233     };
234   }
235 }
236