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