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