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 private EditText mUsernameEditText; 68 private TextView mPasswordLabel; 69 private EditText mPasswordEditText; 70 private TextView mOutput; 71 private Button mLoginButton; 72 private Button mSaveButton; 73 private Button mCancelButton; 74 private Button mClearButton; 75 private 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 } 252 forceAutofillOnUsername()253 public void forceAutofillOnUsername() { 254 syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mUsernameEditText)); 255 } 256 forceAutofillOnPassword()257 public void forceAutofillOnPassword() { 258 syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mPasswordEditText)); 259 } 260 261 /** 262 * Visits the {@code username_label} in the UiThread. 263 */ onUsernameLabel(Visitor<TextView> v)264 public void onUsernameLabel(Visitor<TextView> v) { 265 syncRunOnUiThread(() -> v.visit(mUsernameLabel)); 266 } 267 268 /** 269 * Visits the {@code username} in the UiThread. 270 */ onUsername(Visitor<EditText> v)271 public void onUsername(Visitor<EditText> v) { 272 syncRunOnUiThread(() -> v.visit(mUsernameEditText)); 273 } 274 275 @Override clearFocus()276 public void clearFocus() { 277 syncRunOnUiThread(() -> ((View) mUsernameContainer.getParent()).requestFocus()); 278 } 279 280 /** 281 * Gets the {@code username_label} view. 282 */ getUsernameLabel()283 public TextView getUsernameLabel() { 284 return mUsernameLabel; 285 } 286 287 /** 288 * Gets the {@code username} view. 289 */ getUsername()290 public EditText getUsername() { 291 return mUsernameEditText; 292 } 293 294 /** 295 * Visits the {@code password_label} in the UiThread. 296 */ onPasswordLabel(Visitor<TextView> v)297 public void onPasswordLabel(Visitor<TextView> v) { 298 syncRunOnUiThread(() -> v.visit(mPasswordLabel)); 299 } 300 301 /** 302 * Visits the {@code password} in the UiThread. 303 */ onPassword(Visitor<EditText> v)304 public void onPassword(Visitor<EditText> v) { 305 syncRunOnUiThread(() -> v.visit(mPasswordEditText)); 306 } 307 308 /** 309 * Visits the {@code login} button in the UiThread. 310 */ onLogin(Visitor<Button> v)311 public void onLogin(Visitor<Button> v) { 312 syncRunOnUiThread(() -> v.visit(mLoginButton)); 313 } 314 315 /** 316 * Gets the {@code password} view. 317 */ getPassword()318 public EditText getPassword() { 319 return mPasswordEditText; 320 } 321 322 /** 323 * Taps the login button in the UI thread. 324 */ tapLogin()325 public String tapLogin() throws Exception { 326 mLoginLatch = new CountDownLatch(1); 327 syncRunOnUiThread(() -> mLoginButton.performClick()); 328 boolean called = mLoginLatch.await(LOGIN_TIMEOUT_MS, TimeUnit.MILLISECONDS); 329 assertWithMessage("Timeout (%s ms) waiting for login", LOGIN_TIMEOUT_MS) 330 .that(called).isTrue(); 331 return mLoginMessage; 332 } 333 334 /** 335 * Taps the save button in the UI thread. 336 */ tapSave()337 public void tapSave() throws Exception { 338 syncRunOnUiThread(() -> mSaveButton.performClick()); 339 } 340 341 /** 342 * Taps the clear button in the UI thread. 343 */ tapClear()344 public void tapClear() { 345 syncRunOnUiThread(() -> mClearButton.performClick()); 346 } 347 348 /** 349 * Sets the window flags. 350 */ setFlags(int flags)351 public void setFlags(int flags) { 352 Log.d(TAG, "setFlags():" + flags); 353 syncRunOnUiThread(() -> getWindow().setFlags(flags, flags)); 354 } 355 356 /** 357 * Adds a child view to the root container. 358 */ addChild(View child)359 public void addChild(View child) { 360 Log.d(TAG, "addChild(" + child + "): id=" + child.getAutofillId()); 361 final ViewGroup root = (ViewGroup) mUsernameContainer.getParent(); 362 syncRunOnUiThread(() -> root.addView(child)); 363 } 364 365 /** 366 * Holder for the expected auto-fill values. 367 */ 368 private final class FillExpectation { 369 private final OneTimeTextWatcher ccUsernameWatcher; 370 private final OneTimeTextWatcher ccPasswordWatcher; 371 FillExpectation(String username, String password)372 private FillExpectation(String username, String password) { 373 ccUsernameWatcher = username == null ? null 374 : new OneTimeTextWatcher("username", mUsernameEditText, username); 375 ccPasswordWatcher = password == null ? null 376 : new OneTimeTextWatcher("password", mPasswordEditText, password); 377 } 378 FillExpectation(String username)379 private FillExpectation(String username) { 380 this(username, null); 381 } 382 } 383 } 384