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