• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.google.common.truth.Truth.assertWithMessage;
19 
20 import android.content.Context;
21 import android.content.Intent;
22 import android.os.Bundle;
23 import android.text.TextUtils;
24 import android.util.Log;
25 import android.view.View;
26 import android.view.View.OnClickListener;
27 import android.view.ViewGroup;
28 import android.view.inputmethod.InputMethodManager;
29 import android.widget.Button;
30 import android.widget.EditText;
31 import android.widget.LinearLayout;
32 import android.widget.TextView;
33 
34 import java.util.concurrent.CountDownLatch;
35 import java.util.concurrent.TimeUnit;
36 
37 /**
38  * Activity that has the following fields:
39  *
40  * <ul>
41  *   <li>Username EditText (id: username, no input-type)
42  *   <li>Password EditText (id: "username", input-type textPassword)
43  *   <li>Clear Button
44  *   <li>Save Button
45  *   <li>Login Button
46  * </ul>
47  */
48 public class LoginActivity extends AbstractAutoFillActivity {
49 
50     private static final String TAG = "LoginActivity";
51     private static String WELCOME_TEMPLATE = "Welcome to the new activity, %s!";
52     private static final long LOGIN_TIMEOUT_MS = 1000;
53 
54     public static final String ID_USERNAME_CONTAINER = "username_container";
55     public static final String AUTHENTICATION_MESSAGE = "Authentication failed. D'OH!";
56     public static final String BACKDOOR_USERNAME = "LemmeIn";
57     public static final String BACKDOOR_PASSWORD_SUBSTRING = "pass";
58 
59     private static LoginActivity sCurrentActivity;
60 
61     private LinearLayout mUsernameContainer;
62     private TextView mUsernameLabel;
63     private EditText mUsernameEditText;
64     private TextView mPasswordLabel;
65     private EditText mPasswordEditText;
66     private TextView mOutput;
67     private Button mLoginButton;
68     private Button mSaveButton;
69     private Button mCancelButton;
70     private Button mClearButton;
71     private FillExpectation mExpectation;
72 
73     // State used to synchronously get the result of a login attempt.
74     private CountDownLatch mLoginLatch;
75     private String mLoginMessage;
76 
77     /**
78      * Gets the expected welcome message for a given username.
79      */
getWelcomeMessage(String username)80     public static String getWelcomeMessage(String username) {
81         return String.format(WELCOME_TEMPLATE,  username);
82     }
83 
84     /**
85      * Gests the latest instance.
86      *
87      * <p>Typically used in test cases that rotates the activity
88      */
89     @SuppressWarnings("unchecked") // Its up to caller to make sure it's setting the right one
getCurrentActivity()90     public static <T extends LoginActivity> T getCurrentActivity() {
91         return (T) sCurrentActivity;
92     }
93 
94     @Override
onCreate(Bundle savedInstanceState)95     protected void onCreate(Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97         setContentView(getContentView());
98 
99         mUsernameContainer = findViewById(R.id.username_container);
100         mLoginButton = findViewById(R.id.login);
101         mSaveButton = findViewById(R.id.save);
102         mClearButton = findViewById(R.id.clear);
103         mCancelButton = findViewById(R.id.cancel);
104         mUsernameLabel = findViewById(R.id.username_label);
105         mUsernameEditText = findViewById(R.id.username);
106         mPasswordLabel = findViewById(R.id.password_label);
107         mPasswordEditText = findViewById(R.id.password);
108         mOutput = findViewById(R.id.output);
109 
110         mLoginButton.setOnClickListener((v) -> login());
111         mSaveButton.setOnClickListener((v) -> save());
112         mClearButton.setOnClickListener((v) -> {
113             mUsernameEditText.setText("");
114             mPasswordEditText.setText("");
115             mOutput.setText("");
116             getAutofillManager().cancel();
117         });
118         mCancelButton.setOnClickListener((OnClickListener) v -> finish());
119 
120         sCurrentActivity = this;
121     }
122 
getContentView()123     protected int getContentView() {
124         return R.layout.login_activity;
125     }
126 
127     /**
128      * Emulates a login action.
129      */
login()130     private void login() {
131         final String username = mUsernameEditText.getText().toString();
132         final String password = mPasswordEditText.getText().toString();
133         final boolean valid = username.equals(password)
134                 || (TextUtils.isEmpty(username) && TextUtils.isEmpty(password))
135                 || password.contains(BACKDOOR_PASSWORD_SUBSTRING)
136                 || username.equals(BACKDOOR_USERNAME);
137 
138         if (valid) {
139             Log.d(TAG, "login ok: " + username);
140             final Intent intent = new Intent(this, WelcomeActivity.class);
141             final String message = getWelcomeMessage(username);
142             intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, message);
143             setLoginMessage(message);
144             startActivity(intent);
145             finish();
146         } else {
147             Log.d(TAG, "login failed: " + AUTHENTICATION_MESSAGE);
148             mOutput.setText(AUTHENTICATION_MESSAGE);
149             setLoginMessage(AUTHENTICATION_MESSAGE);
150         }
151     }
152 
setLoginMessage(String message)153     private void setLoginMessage(String message) {
154         Log.d(TAG, "setLoginMessage(): " + message);
155         if (mLoginLatch != null) {
156             mLoginMessage = message;
157             mLoginLatch.countDown();
158         }
159     }
160 
161     /**
162      * Explicitly forces the AutofillManager to save the username and password.
163      */
save()164     private void save() {
165         final InputMethodManager imm = (InputMethodManager) getSystemService(
166                 Context.INPUT_METHOD_SERVICE);
167         imm.hideSoftInputFromWindow(mUsernameEditText.getWindowToken(), 0);
168         getAutofillManager().commit();
169     }
170 
171     /**
172      * Sets the expectation for an autofill request (for all fields), so it can be asserted through
173      * {@link #assertAutoFilled()} later.
174      */
expectAutoFill(String username, String password)175     public void expectAutoFill(String username, String password) {
176         mExpectation = new FillExpectation(username, password);
177         mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
178         mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
179     }
180 
181     /**
182      * Sets the expectation for an autofill request (for username only), so it can be asserted
183      * through {@link #assertAutoFilled()} later.
184      */
expectAutoFill(String username)185     public void expectAutoFill(String username) {
186         mExpectation = new FillExpectation(username);
187         mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
188     }
189 
190     /**
191      * Sets the expectation for an autofill request (for password only), so it can be asserted
192      * through {@link #assertAutoFilled()} later.
193      */
expectPasswordAutoFill(String password)194     public void expectPasswordAutoFill(String password) {
195         mExpectation = new FillExpectation(null, password);
196         mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
197     }
198 
199     /**
200      * Asserts the activity was auto-filled with the values passed to
201      * {@link #expectAutoFill(String, String)}.
202      */
assertAutoFilled()203     public void assertAutoFilled() throws Exception {
204         assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
205         if (mExpectation.ccUsernameWatcher != null) {
206             mExpectation.ccUsernameWatcher.assertAutoFilled();
207         }
208         if (mExpectation.ccPasswordWatcher != null) {
209             mExpectation.ccPasswordWatcher.assertAutoFilled();
210         }
211     }
212 
forceAutofillOnUsername()213     public void forceAutofillOnUsername() {
214         syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mUsernameEditText));
215     }
216 
forceAutofillOnPassword()217     public void forceAutofillOnPassword() {
218         syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mPasswordEditText));
219     }
220 
221     /**
222      * Visits the {@code username_label} in the UiThread.
223      */
onUsernameLabel(Visitor<TextView> v)224     public void onUsernameLabel(Visitor<TextView> v) {
225         syncRunOnUiThread(() -> v.visit(mUsernameLabel));
226     }
227 
228     /**
229      * Visits the {@code username} in the UiThread.
230      */
onUsername(Visitor<EditText> v)231     public void onUsername(Visitor<EditText> v) {
232         syncRunOnUiThread(() -> v.visit(mUsernameEditText));
233     }
234 
235     /**
236      * Clears focus from input fields by focusing on the parent layout.
237      */
clearFocus()238     public void clearFocus() {
239         syncRunOnUiThread(() -> ((View) mUsernameContainer.getParent()).requestFocus());
240     }
241 
242     /**
243      * Gets the {@code username_label} view.
244      */
getUsernameLabel()245     public TextView getUsernameLabel() {
246         return mUsernameLabel;
247     }
248 
249     /**
250      * Gets the {@code username} view.
251      */
getUsername()252     public EditText getUsername() {
253         return mUsernameEditText;
254     }
255 
256     /**
257      * Visits the {@code password_label} in the UiThread.
258      */
onPasswordLabel(Visitor<TextView> v)259     public void onPasswordLabel(Visitor<TextView> v) {
260         syncRunOnUiThread(() -> v.visit(mPasswordLabel));
261     }
262 
263     /**
264      * Visits the {@code password} in the UiThread.
265      */
onPassword(Visitor<EditText> v)266     public void onPassword(Visitor<EditText> v) {
267         syncRunOnUiThread(() -> v.visit(mPasswordEditText));
268     }
269 
270     /**
271      * Visits the {@code login} button in the UiThread.
272      */
onLogin(Visitor<Button> v)273     public void onLogin(Visitor<Button> v) {
274         syncRunOnUiThread(() -> v.visit(mLoginButton));
275     }
276 
277     /**
278      * Gets the {@code password} view.
279      */
getPassword()280     public EditText getPassword() {
281         return mPasswordEditText;
282     }
283 
284     /**
285      * Taps the login button in the UI thread.
286      */
tapLogin()287     public String tapLogin() throws Exception {
288         mLoginLatch = new CountDownLatch(1);
289         syncRunOnUiThread(() -> mLoginButton.performClick());
290         boolean called = mLoginLatch.await(LOGIN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
291         assertWithMessage("Timeout (%s ms) waiting for login", LOGIN_TIMEOUT_MS)
292                 .that(called).isTrue();
293         return mLoginMessage;
294     }
295 
296     /**
297      * Taps the save button in the UI thread.
298      */
tapSave()299     public void tapSave() throws Exception {
300         syncRunOnUiThread(() -> mSaveButton.performClick());
301     }
302 
303     /**
304      * Taps the clear button in the UI thread.
305      */
tapClear()306     public void tapClear() {
307         syncRunOnUiThread(() -> mClearButton.performClick());
308     }
309 
310     /**
311      * Sets the window flags.
312      */
setFlags(int flags)313     public void setFlags(int flags) {
314         Log.d(TAG, "setFlags():" + flags);
315         syncRunOnUiThread(() -> getWindow().setFlags(flags, flags));
316     }
317 
318     /**
319      * Adds a child view to the root container.
320      */
addChild(View child)321     public void addChild(View child) {
322         Log.d(TAG, "addChild(" + child + "): id=" + child.getAutofillId());
323         final ViewGroup root = (ViewGroup) mUsernameContainer.getParent();
324         syncRunOnUiThread(() -> root.addView(child));
325     }
326 
327     /**
328      * Holder for the expected auto-fill values.
329      */
330     private final class FillExpectation {
331         private final OneTimeTextWatcher ccUsernameWatcher;
332         private final OneTimeTextWatcher ccPasswordWatcher;
333 
FillExpectation(String username, String password)334         private FillExpectation(String username, String password) {
335             ccUsernameWatcher = username == null ? null
336                     : new OneTimeTextWatcher("username", mUsernameEditText, username);
337             ccPasswordWatcher = password == null ? null
338                     : new OneTimeTextWatcher("password", mPasswordEditText, password);
339         }
340 
FillExpectation(String username)341         private FillExpectation(String username) {
342             this(username, null);
343         }
344     }
345 }
346