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     @Override
clearFocus()236     public void clearFocus() {
237         syncRunOnUiThread(() -> ((View) mUsernameContainer.getParent()).requestFocus());
238     }
239 
240     /**
241      * Gets the {@code username_label} view.
242      */
getUsernameLabel()243     public TextView getUsernameLabel() {
244         return mUsernameLabel;
245     }
246 
247     /**
248      * Gets the {@code username} view.
249      */
getUsername()250     public EditText getUsername() {
251         return mUsernameEditText;
252     }
253 
254     /**
255      * Visits the {@code password_label} in the UiThread.
256      */
onPasswordLabel(Visitor<TextView> v)257     public void onPasswordLabel(Visitor<TextView> v) {
258         syncRunOnUiThread(() -> v.visit(mPasswordLabel));
259     }
260 
261     /**
262      * Visits the {@code password} in the UiThread.
263      */
onPassword(Visitor<EditText> v)264     public void onPassword(Visitor<EditText> v) {
265         syncRunOnUiThread(() -> v.visit(mPasswordEditText));
266     }
267 
268     /**
269      * Visits the {@code login} button in the UiThread.
270      */
onLogin(Visitor<Button> v)271     public void onLogin(Visitor<Button> v) {
272         syncRunOnUiThread(() -> v.visit(mLoginButton));
273     }
274 
275     /**
276      * Gets the {@code password} view.
277      */
getPassword()278     public EditText getPassword() {
279         return mPasswordEditText;
280     }
281 
282     /**
283      * Taps the login button in the UI thread.
284      */
tapLogin()285     public String tapLogin() throws Exception {
286         mLoginLatch = new CountDownLatch(1);
287         syncRunOnUiThread(() -> mLoginButton.performClick());
288         boolean called = mLoginLatch.await(LOGIN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
289         assertWithMessage("Timeout (%s ms) waiting for login", LOGIN_TIMEOUT_MS)
290                 .that(called).isTrue();
291         return mLoginMessage;
292     }
293 
294     /**
295      * Taps the save button in the UI thread.
296      */
tapSave()297     public void tapSave() throws Exception {
298         syncRunOnUiThread(() -> mSaveButton.performClick());
299     }
300 
301     /**
302      * Taps the clear button in the UI thread.
303      */
tapClear()304     public void tapClear() {
305         syncRunOnUiThread(() -> mClearButton.performClick());
306     }
307 
308     /**
309      * Sets the window flags.
310      */
setFlags(int flags)311     public void setFlags(int flags) {
312         Log.d(TAG, "setFlags():" + flags);
313         syncRunOnUiThread(() -> getWindow().setFlags(flags, flags));
314     }
315 
316     /**
317      * Adds a child view to the root container.
318      */
addChild(View child)319     public void addChild(View child) {
320         Log.d(TAG, "addChild(" + child + "): id=" + child.getAutofillId());
321         final ViewGroup root = (ViewGroup) mUsernameContainer.getParent();
322         syncRunOnUiThread(() -> root.addView(child));
323     }
324 
325     /**
326      * Holder for the expected auto-fill values.
327      */
328     private final class FillExpectation {
329         private final OneTimeTextWatcher ccUsernameWatcher;
330         private final OneTimeTextWatcher ccPasswordWatcher;
331 
FillExpectation(String username, String password)332         private FillExpectation(String username, String password) {
333             ccUsernameWatcher = username == null ? null
334                     : new OneTimeTextWatcher("username", mUsernameEditText, username);
335             ccPasswordWatcher = password == null ? null
336                     : new OneTimeTextWatcher("password", mPasswordEditText, password);
337         }
338 
FillExpectation(String username)339         private FillExpectation(String username) {
340             this(username, null);
341         }
342     }
343 }
344