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