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