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