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