1 /* 2 * Copyright (C) 2017 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 package android.autofillservice.cts; 17 18 import static android.widget.ArrayAdapter.createFromResource; 19 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import android.content.Intent; 23 import android.os.Bundle; 24 import android.util.Log; 25 import android.widget.ArrayAdapter; 26 import android.widget.Button; 27 import android.widget.CheckBox; 28 import android.widget.EditText; 29 import android.widget.RadioButton; 30 import android.widget.RadioGroup; 31 import android.widget.Spinner; 32 33 import java.util.concurrent.CountDownLatch; 34 import java.util.concurrent.TimeUnit; 35 36 /** 37 * Activity that has the following fields: 38 * 39 * <ul> 40 * <li>Credit Card Number EditText (id: cc_numberusername, no input-type) 41 * <li>Credit Card Expiration EditText (id: cc_expiration, no input-type) 42 * <li>Address RadioGroup (id: addess, no autofill-type) 43 * <li>Save Credit Card CheckBox (id: save_cc, no autofill-type) 44 * <li>Clear Button 45 * <li>Buy Button 46 * </ul> 47 */ 48 public class CheckoutActivity extends AbstractAutoFillActivity { 49 private static final String TAG = "CheckoutActivity"; 50 private static final long BUY_TIMEOUT_MS = 1000; 51 52 static final String ID_CC_NUMBER = "cc_number"; 53 static final String ID_CC_EXPIRATION = "cc_expiration"; 54 static final String ID_ADDRESS = "address"; 55 static final String ID_HOME_ADDRESS = "home_address"; 56 static final String ID_WORK_ADDRESS = "work_address"; 57 static final String ID_SAVE_CC = "save_cc"; 58 59 static final int INDEX_ADDRESS_HOME = 0; 60 static final int INDEX_ADDRESS_WORK = 1; 61 62 static final int INDEX_CC_EXPIRATION_YESTERDAY = 0; 63 static final int INDEX_CC_EXPIRATION_TODAY = 1; 64 static final int INDEX_CC_EXPIRATION_TOMORROW = 2; 65 static final int INDEX_CC_EXPIRATION_NEVER = 3; 66 67 private EditText mCcNumber; 68 private Spinner mCcExpiration; 69 private ArrayAdapter<CharSequence> mCcExpirationAdapter; 70 private RadioGroup mAddress; 71 private RadioButton mHomeAddress; 72 private CheckBox mSaveCc; 73 private Button mBuyButton; 74 private Button mClearButton; 75 76 private FillExpectation mExpectation; 77 private CountDownLatch mBuyLatch; 78 79 private static CheckoutActivity sInstance; 80 81 public CheckoutActivity() { 82 sInstance = this; 83 } 84 85 @Override 86 protected void onCreate(Bundle savedInstanceState) { 87 super.onCreate(savedInstanceState); 88 89 setContentView(getContentView()); 90 91 mCcNumber = findViewById(R.id.cc_number); 92 mCcExpiration = findViewById(R.id.cc_expiration); 93 mAddress = findViewById(R.id.address); 94 mHomeAddress = findViewById(R.id.home_address); 95 mSaveCc = findViewById(R.id.save_cc); 96 mBuyButton = findViewById(R.id.buy); 97 mClearButton = findViewById(R.id.clear); 98 99 mCcExpirationAdapter = createFromResource(this, 100 R.array.cc_expiration_values, android.R.layout.simple_spinner_item); 101 mCcExpirationAdapter 102 .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 103 mCcExpiration.setAdapter(mCcExpirationAdapter); 104 105 mBuyButton.setOnClickListener((v) -> buy()); 106 mClearButton.setOnClickListener((v) -> resetFields()); 107 } 108 109 @Override 110 public void finish() { 111 super.finish(); 112 113 sInstance = null; 114 } 115 116 static void finishIt(UiBot uiBot) { 117 if (sInstance != null) { 118 Log.d(TAG, "So long and thanks for all the fish!"); 119 sInstance.finish(); 120 uiBot.assertGoneByRelativeId(ID_CC_NUMBER, Timeouts.ACTIVITY_RESURRECTION); 121 } 122 } 123 124 protected int getContentView() { 125 return R.layout.checkout_activity; 126 } 127 128 /** 129 * Resets the values of the input fields. 130 */ 131 private void resetFields() { 132 mCcNumber.setText(""); 133 mCcExpiration.setSelection(0, false); 134 mAddress.clearCheck(); 135 mSaveCc.setChecked(false); 136 } 137 138 /** 139 * Emulates a buy action. 140 */ 141 private void buy() { 142 final Intent intent = new Intent(this, WelcomeActivity.class); 143 intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "Thank you an come again!"); 144 startActivity(intent); 145 if (mBuyLatch != null) { 146 // Latch is not set when activity launched outside tests 147 mBuyLatch.countDown(); 148 } 149 finish(); 150 } 151 152 /** 153 * Sets the expectation for an auto-fill request, so it can be asserted through 154 * {@link #assertAutoFilled()} later. 155 */ 156 void expectAutoFill(String ccNumber, int ccExpirationIndex, int addressId, boolean saveCc) { 157 mExpectation = new FillExpectation(ccNumber, ccExpirationIndex, addressId, saveCc); 158 mCcNumber.addTextChangedListener(mExpectation.ccNumberWatcher); 159 mCcExpiration.setOnItemSelectedListener(mExpectation.ccExpirationListener); 160 mAddress.setOnCheckedChangeListener(mExpectation.addressListener); 161 mSaveCc.setOnCheckedChangeListener(mExpectation.saveCcListener); 162 } 163 164 /** 165 * Asserts the activity was auto-filled with the values passed to 166 * {@link #expectAutoFill(String, int, int, boolean)}. 167 */ 168 void assertAutoFilled() throws Exception { 169 assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull(); 170 mExpectation.ccNumberWatcher.assertAutoFilled(); 171 mExpectation.ccExpirationListener.assertAutoFilled(); 172 mExpectation.addressListener.assertAutoFilled(); 173 mExpectation.saveCcListener.assertAutoFilled(); 174 } 175 176 /** 177 * Visits the {@code ccNumber} in the UiThread. 178 */ 179 void onCcNumber(Visitor<EditText> v) { 180 syncRunOnUiThread(() -> v.visit(mCcNumber)); 181 } 182 183 /** 184 * Visits the {@code ccExpirationDate} in the UiThread. 185 */ 186 void onCcExpiration(Visitor<Spinner> v) { 187 syncRunOnUiThread(() -> v.visit(mCcExpiration)); 188 } 189 190 /** 191 * Visits the {@code ccExpirationDate} adapter in the UiThread. 192 */ 193 void onCcExpirationAdapter(Visitor<ArrayAdapter<CharSequence>> v) { 194 syncRunOnUiThread(() -> v.visit(mCcExpirationAdapter)); 195 } 196 197 /** 198 * Visits the {@code address} in the UiThread. 199 */ 200 void onAddress(Visitor<RadioGroup> v) { 201 syncRunOnUiThread(() -> v.visit(mAddress)); 202 } 203 204 /** 205 * Visits the {@code homeAddress} in the UiThread. 206 */ 207 void onHomeAddress(Visitor<RadioButton> v) { 208 syncRunOnUiThread(() -> v.visit(mHomeAddress)); 209 } 210 211 /** 212 * Visits the {@code saveCC} in the UiThread. 213 */ 214 void onSaveCc(Visitor<CheckBox> v) { 215 syncRunOnUiThread(() -> v.visit(mSaveCc)); 216 } 217 218 /** 219 * Taps the buy button in the UI thread. 220 */ 221 void tapBuy() throws Exception { 222 mBuyLatch = new CountDownLatch(1); 223 syncRunOnUiThread(() -> mBuyButton.performClick()); 224 boolean called = mBuyLatch.await(BUY_TIMEOUT_MS, TimeUnit.MILLISECONDS); 225 assertWithMessage("Timeout (%s ms) waiting for buy action", BUY_TIMEOUT_MS) 226 .that(called).isTrue(); 227 } 228 229 EditText getCcNumber() { 230 return mCcNumber; 231 } 232 233 Spinner getCcExpiration() { 234 return mCcExpiration; 235 } 236 237 ArrayAdapter<CharSequence> getCcExpirationAdapter() { 238 return mCcExpirationAdapter; 239 } 240 241 /** 242 * Holder for the expected auto-fill values. 243 */ 244 private final class FillExpectation { 245 private final OneTimeTextWatcher ccNumberWatcher; 246 private final OneTimeSpinnerListener ccExpirationListener; 247 private final OneTimeRadioGroupListener addressListener; 248 private final OneTimeCompoundButtonListener saveCcListener; 249 250 private FillExpectation(String ccNumber, int ccExpirationIndex, int addressId, 251 boolean saveCc) { 252 this.ccNumberWatcher = new OneTimeTextWatcher("ccNumber", mCcNumber, ccNumber); 253 this.ccExpirationListener = 254 new OneTimeSpinnerListener("ccExpiration", mCcExpiration, ccExpirationIndex); 255 addressListener = new OneTimeRadioGroupListener("address", mAddress, addressId); 256 saveCcListener = new OneTimeCompoundButtonListener("saveCc", mSaveCc, saveCc); 257 } 258 } 259 } 260