xref: /aosp_15_r20/cts/tests/autofillservice/src/android/autofillservice/cts/activities/LoginActivity.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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 com.google.common.truth.Truth.assertWithMessage;
19 
20 import android.autofillservice.cts.R;
21 import android.autofillservice.cts.testcore.OneTimeTextWatcher;
22 import android.autofillservice.cts.testcore.Visitor;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.os.Bundle;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.view.View;
29 import android.view.View.OnClickListener;
30 import android.view.ViewGroup;
31 import android.view.inputmethod.InputMethodManager;
32 import android.widget.Button;
33 import android.widget.EditText;
34 import android.widget.LinearLayout;
35 import android.widget.TextView;
36 
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.TimeUnit;
39 
40 /**
41  * Activity that has the following fields:
42  *
43  * <ul>
44  *   <li>Username EditText (id: username, no input-type)
45  *   <li>Password EditText (id: "username", input-type textPassword)
46  *   <li>Clear Button
47  *   <li>Save Button
48  *   <li>Login Button
49  * </ul>
50  */
51 public class LoginActivity extends AbstractAutoFillActivity {
52 
53     private static final String TAG = "LoginActivity";
54     private static final long LOGIN_TIMEOUT_MS = 1000;
55 
56     public static final String ID_USERNAME_CONTAINER = "username_container";
57     public static final String AUTHENTICATION_MESSAGE = "Authentication failed. D'OH!";
58     public static final String BACKDOOR_USERNAME = "LemmeIn";
59     public static final String BACKDOOR_PASSWORD_SUBSTRING = "pass";
60 
61     private static String sWelcomeTemplate = "Welcome to the new activity, %s!";
62 
63     private static LoginActivity sCurrentActivity;
64 
65     private LinearLayout mUsernameContainer;
66     private TextView mUsernameLabel;
67     EditText mUsernameEditText;
68     private TextView mPasswordLabel;
69     EditText mPasswordEditText;
70     private TextView mOutput;
71     private Button mLoginButton;
72     private Button mSaveButton;
73     private Button mCancelButton;
74     private Button mClearButton;
75     public Button mInvisibleButton;
76     FillExpectation mExpectation;
77 
78     // State used to synchronously get the result of a login attempt.
79     private CountDownLatch mLoginLatch;
80     private String mLoginMessage;
81 
82     /**
83      * Gets the expected welcome message for a given username.
84      */
getWelcomeMessage(String username)85     public static String getWelcomeMessage(String username) {
86         return String.format(sWelcomeTemplate,  username);
87     }
88 
89     /**
90      * Gests the latest instance.
91      *
92      * <p>Typically used in test cases that rotates the activity
93      */
94     @SuppressWarnings("unchecked") // Its up to caller to make sure it's setting the right one
getCurrentActivity()95     public static <T extends LoginActivity> T getCurrentActivity() {
96         return (T) sCurrentActivity;
97     }
98 
99     @Override
onCreate(Bundle savedInstanceState)100     protected void onCreate(Bundle savedInstanceState) {
101         super.onCreate(savedInstanceState);
102         setContentView(getContentView());
103 
104         mUsernameContainer = findViewById(R.id.username_container);
105         mLoginButton = findViewById(R.id.login);
106         mSaveButton = findViewById(R.id.save);
107         mClearButton = findViewById(R.id.clear);
108         mCancelButton = findViewById(R.id.cancel);
109         mInvisibleButton = findViewById(R.id.make_views_invisible);
110         mUsernameLabel = findViewById(R.id.username_label);
111         mUsernameEditText = findViewById(R.id.username);
112         mPasswordLabel = findViewById(R.id.password_label);
113         mPasswordEditText = findViewById(R.id.password);
114         mOutput = findViewById(R.id.output);
115 
116         mLoginButton.setOnClickListener((v) -> login());
117         mSaveButton.setOnClickListener((v) -> save());
118         mClearButton.setOnClickListener((v) -> {
119             mUsernameEditText.setText("");
120             mPasswordEditText.setText("");
121             mOutput.setText("");
122             getAutofillManager().cancel();
123         });
124         mCancelButton.setOnClickListener((OnClickListener) v -> finish());
125 
126         // This class is subclassed with various different layouts. So we add a check to see if the
127         // layout inflated has the invisible button first.
128         if (mInvisibleButton != null) {
129             mInvisibleButton.setOnClickListener((v) -> makeEditTextViewsInvisible());
130         }
131 
132         sCurrentActivity = this;
133     }
134 
getContentView()135     protected int getContentView() {
136         return R.layout.login_activity;
137     }
138 
139     /**
140      * Emulates a login action.
141      */
login()142     private void login() {
143         final String username = mUsernameEditText.getText().toString();
144         final String password = mPasswordEditText.getText().toString();
145         final boolean valid = username.equals(password)
146                 || (TextUtils.isEmpty(username) && TextUtils.isEmpty(password))
147                 || password.contains(BACKDOOR_PASSWORD_SUBSTRING)
148                 || username.equals(BACKDOOR_USERNAME);
149 
150         if (valid) {
151             Log.d(TAG, "login ok: " + username);
152             final Intent intent = new Intent(this, WelcomeActivity.class);
153             final String message = getWelcomeMessage(username);
154             intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, message);
155             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
156             setLoginMessage(message);
157             startActivity(intent);
158             finish();
159         } else {
160             Log.d(TAG, "login failed: " + AUTHENTICATION_MESSAGE);
161             mOutput.setText(AUTHENTICATION_MESSAGE);
162             setLoginMessage(AUTHENTICATION_MESSAGE);
163         }
164     }
165 
makeEditTextViewsInvisible()166     private void makeEditTextViewsInvisible() {
167         // Make the views invisible
168         Log.v(TAG, "makeEditTextViewsInvisible() onClick()");
169         mPasswordEditText.setVisibility(View.INVISIBLE);
170         mUsernameEditText.setVisibility(View.INVISIBLE);
171         Log.v(TAG, "makeEditTextViewsInvisible() username and password views are invisible");
172     }
173 
setLoginMessage(String message)174     private void setLoginMessage(String message) {
175         Log.d(TAG, "setLoginMessage(): " + message);
176         if (mLoginLatch != null) {
177             mLoginMessage = message;
178             mLoginLatch.countDown();
179         }
180     }
181 
182     /**
183      * Explicitly forces the AutofillManager to save the username and password.
184      */
save()185     private void save() {
186         final InputMethodManager imm = (InputMethodManager) getSystemService(
187                 Context.INPUT_METHOD_SERVICE);
188         imm.hideSoftInputFromWindow(mUsernameEditText.getWindowToken(), 0);
189         getAutofillManager().commit();
190     }
191 
192     /**
193      * Sets the expectation for an autofill request (for all fields), so it can be asserted through
194      * {@link #assertAutoFilled()} later.
195      */
expectAutoFill(String username, String password)196     public void expectAutoFill(String username, String password) {
197         mExpectation = new FillExpectation(username, password);
198         mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
199         mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
200     }
201 
202     /**
203      * Sets the expectation for an autofill request (for username only), so it can be asserted
204      * through {@link #assertAutoFilled()} later.
205      *
206      * <p><strong>NOTE: </strong>This method checks the result of text change, it should not call
207      * this method too early, it may cause test fail. Call this method before checking autofill
208      * behavior.
209      * <pre>
210      * An example usage is:
211      * <code>
212      *  public void testAutofill() throws Exception {
213      *      // Enable service and trigger autofill
214      *      enableService();
215      *      final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
216      *                 .addDataset(new CannedFillResponse.CannedDataset.Builder()
217      *                         .setField(ID_USERNAME, "test")
218      *                         .setField(ID_PASSWORD, "tweet")
219      *                         .setPresentation(createPresentation("Second Dude"))
220      *                         .setInlinePresentation(createInlinePresentation("Second Dude"))
221      *                         .build());
222      *      sReplier.addResponse(builder.build());
223      *      mUiBot.selectByRelativeId(ID_USERNAME);
224      *      sReplier.getNextFillRequest();
225      *      // Filter suggestion
226      *      mActivity.onUsername((v) -> v.setText("t"));
227      *      mUiBot.assertDatasets("Second Dude");
228      *
229      *      // Call expectAutoFill() before checking autofill behavior
230      *      mActivity.expectAutoFill("test", "tweet");
231      *      mUiBot.selectDataset("Second Dude");
232      *      mActivity.assertAutoFilled();
233      *  }
234      * </code>
235      * </pre>
236      */
expectAutoFill(String username)237     public void expectAutoFill(String username) {
238         mExpectation = new FillExpectation(username);
239         mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
240     }
241 
242     /**
243      * Sets the expectation for an autofill request (for password only), so it can be asserted
244      * through {@link #assertAutoFilled()} later.
245      *
246      * <p><strong>NOTE: </strong>This method checks the result of text change, it should not call
247      * this method too early, it may cause test fail. Call this method before checking autofill
248      * behavior. {@See #expectAutoFill(String)} for how it should be used.
249      */
expectPasswordAutoFill(String password)250     public void expectPasswordAutoFill(String password) {
251         mExpectation = new FillExpectation(null, password);
252         mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
253     }
254 
255     /**
256      * Asserts the activity was auto-filled with the values passed to
257      * {@link #expectAutoFill(String, String)}.
258      */
assertAutoFilled()259     public void assertAutoFilled() throws Exception {
260         assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
261         if (mExpectation.ccUsernameWatcher != null) {
262             mExpectation.ccUsernameWatcher.assertAutoFilled();
263         }
264         if (mExpectation.ccPasswordWatcher != null) {
265             mExpectation.ccPasswordWatcher.assertAutoFilled();
266         }
267         if (mExpectation.mCustomFieldWatcher != null) {
268             mExpectation.mCustomFieldWatcher.assertAutoFilled();
269         }
270     }
271 
forceAutofillOnUsername()272     public void forceAutofillOnUsername() {
273         syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mUsernameEditText));
274     }
275 
forceAutofillOnPassword()276     public void forceAutofillOnPassword() {
277         syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mPasswordEditText));
278     }
279 
280     /**
281      * Visits the {@code username_label} in the UiThread.
282      */
onUsernameLabel(Visitor<TextView> v)283     public void onUsernameLabel(Visitor<TextView> v) {
284         syncRunOnUiThread(() -> v.visit(mUsernameLabel));
285     }
286 
287     /**
288      * Visits the {@code username} in the UiThread.
289      */
onUsername(Visitor<EditText> v)290     public void onUsername(Visitor<EditText> v) {
291         syncRunOnUiThread(() -> v.visit(mUsernameEditText));
292     }
293 
294     @Override
clearFocus()295     public void clearFocus() {
296         syncRunOnUiThread(() -> ((View) mUsernameContainer.getParent()).requestFocus());
297     }
298 
299     /**
300      * Gets the {@code username_label} view.
301      */
getUsernameLabel()302     public TextView getUsernameLabel() {
303         return mUsernameLabel;
304     }
305 
306     /**
307      * Gets the {@code username} view.
308      */
getUsername()309     public EditText getUsername() {
310         return mUsernameEditText;
311     }
312 
313     /**
314      * Visits the {@code password_label} in the UiThread.
315      */
onPasswordLabel(Visitor<TextView> v)316     public void onPasswordLabel(Visitor<TextView> v) {
317         syncRunOnUiThread(() -> v.visit(mPasswordLabel));
318     }
319 
320     /**
321      * Visits the {@code password} in the UiThread.
322      */
onPassword(Visitor<EditText> v)323     public void onPassword(Visitor<EditText> v) {
324         syncRunOnUiThread(() -> v.visit(mPasswordEditText));
325     }
326 
327     /**
328      * Visits the {@code login} button in the UiThread.
329      */
onLogin(Visitor<Button> v)330     public void onLogin(Visitor<Button> v) {
331         syncRunOnUiThread(() -> v.visit(mLoginButton));
332     }
333 
334     /**
335      * Visits the {@code cancel} button in the UiThread.
336      */
onCancel(Visitor<Button> v)337     public void onCancel(Visitor<Button> v) {
338         syncRunOnUiThread(() -> v.visit(mCancelButton));
339     }
340 
341     /**
342      * Gets the {@code password} view.
343      */
getPassword()344     public EditText getPassword() {
345         return mPasswordEditText;
346     }
347 
348     /**
349      * Taps the login button in the UI thread.
350      */
tapLogin()351     public String tapLogin() throws Exception {
352         mLoginLatch = new CountDownLatch(1);
353         syncRunOnUiThread(() -> mLoginButton.performClick());
354         boolean called = mLoginLatch.await(LOGIN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
355         assertWithMessage("Timeout (%s ms) waiting for login", LOGIN_TIMEOUT_MS)
356                 .that(called).isTrue();
357         return mLoginMessage;
358     }
359 
360     /**
361      * Taps the save button in the UI thread.
362      */
tapSave()363     public void tapSave() throws Exception {
364         syncRunOnUiThread(() -> mSaveButton.performClick());
365     }
366 
367     /**
368      * Taps the clear button in the UI thread.
369      */
tapClear()370     public void tapClear() {
371         syncRunOnUiThread(() -> mClearButton.performClick());
372     }
373 
374     /**
375      * Sets the window flags.
376      */
setFlags(int flags)377     public void setFlags(int flags) {
378         Log.d(TAG, "setFlags():" + flags);
379         syncRunOnUiThread(() -> getWindow().setFlags(flags, flags));
380     }
381 
382     /**
383      * Adds a child view to the root container.
384      */
addChild(View child)385     public void addChild(View child) {
386         Log.d(TAG, "addChild(" + child + "): id=" + child.getAutofillId());
387         final ViewGroup root = (ViewGroup) mUsernameContainer.getParent();
388         syncRunOnUiThread(() -> root.addView(child));
389     }
390 
391     /**
392      * Set the EditText input or password value and wait until text change.
393      */
setTextAndWaitTextChange(String username, String password)394     public void setTextAndWaitTextChange(String username, String password) throws Exception {
395         expectTextChange(username, password);
396 
397         syncRunOnUiThread(() -> {
398             if (username != null) {
399                 onUsername((v) -> v.setText(username));
400 
401             }
402             if (password != null) {
403                 onPassword((v) -> v.setText(password));
404             }
405         });
406 
407         assertTextChange();
408     }
409 
expectTextChange(String username, String password)410     private void expectTextChange(String username, String password) {
411         expectAutoFill(username, password);
412     }
413 
assertTextChange()414     private void assertTextChange() throws Exception {
415         assertAutoFilled();
416     }
417 
418     /**
419      * Request to hide soft input
420      */
hideSoftInput()421     public void hideSoftInput() {
422         final InputMethodManager imm = (InputMethodManager) getSystemService(
423                 Context.INPUT_METHOD_SERVICE);
424         imm.hideSoftInputFromWindow(mUsernameEditText.getWindowToken(), 0);
425     }
426 
427     /**
428      * Holder for the expected auto-fill values.
429      */
430     final class FillExpectation {
431         private final OneTimeTextWatcher ccUsernameWatcher;
432         private final OneTimeTextWatcher ccPasswordWatcher;
433         final OneTimeTextWatcher mCustomFieldWatcher;
434 
FillExpectation(String username, String password)435         FillExpectation(String username, String password) {
436             ccUsernameWatcher = username == null ? null
437                     : new OneTimeTextWatcher("username", mUsernameEditText, username);
438             ccPasswordWatcher = password == null ? null
439                     : new OneTimeTextWatcher("password", mPasswordEditText, password);
440             mCustomFieldWatcher = null;
441         }
442 
FillExpectation(String type, String value, EditText customField)443         FillExpectation(String type, String value, EditText customField) {
444             ccUsernameWatcher = null;
445             ccPasswordWatcher = null;
446             mCustomFieldWatcher = new OneTimeTextWatcher(type, customField, value);
447         }
448 
FillExpectation(String username)449         private FillExpectation(String username) {
450             this(username, null);
451         }
452     }
453 }
454