• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.autofillservice.cts.activities;
18 
19 import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.NULL;
20 
21 import static com.google.common.truth.Truth.assertWithMessage;
22 
23 import android.app.Activity;
24 import android.app.PendingIntent;
25 import android.app.assist.AssistStructure;
26 import android.autofillservice.cts.R;
27 import android.autofillservice.cts.testcore.CannedFillResponse;
28 import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
29 import android.autofillservice.cts.testcore.Helper;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentSender;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.Parcelable;
37 import android.util.Log;
38 import android.util.SparseArray;
39 import android.view.autofill.AutofillManager;
40 import android.view.inputmethod.InlineSuggestionsRequest;
41 import android.widget.Button;
42 import android.widget.EditText;
43 
44 import com.google.common.base.Preconditions;
45 
46 import java.util.ArrayList;
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.TimeUnit;
49 
50 /**
51  * This class simulates authentication at the dataset at reponse level
52  */
53 public class AuthenticationActivity extends AbstractAutoFillActivity {
54 
55     private static final String TAG = "AuthenticationActivity";
56     private static final String EXTRA_DATASET_ID = "dataset_id";
57     private static final String EXTRA_RESPONSE_ID = "response_id";
58 
59     /**
60      * When launched with this intent, it will pass it back to the
61      * {@link AutofillManager#EXTRA_CLIENT_STATE} of the result.
62      */
63     private static final String EXTRA_OUTPUT_CLIENT_STATE = "output_client_state";
64 
65     /**
66      * When launched with this intent, it will pass it back to the
67      * {@link AutofillManager#EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET} of the result.
68      */
69     private static final String EXTRA_OUTPUT_IS_EPHEMERAL_DATASET = "output_is_ephemeral_dataset";
70 
71     /**
72      * When launched with a non-null intent associated with this extra, the intent will be returned
73      * as the response.
74      */
75     private static final String EXTRA_RESPONSE_INTENT = "response_intent";
76 
77 
78     private static final int MSG_WAIT_FOR_LATCH = 1;
79     private static final int MSG_REQUEST_AUTOFILL = 2;
80 
81     private static Bundle sData;
82     private static InlineSuggestionsRequest sInlineSuggestionsRequest;
83     private static final SparseArray<CannedDataset> sDatasets = new SparseArray<>();
84     private static final SparseArray<CannedFillResponse> sResponses = new SparseArray<>();
85     private static final ArrayList<PendingIntent> sPendingIntents = new ArrayList<>();
86 
87     private static Object sLock = new Object();
88 
89     // Guarded by sLock
90     private static int sResultCode;
91 
92     // Guarded by sLock
93     // Used to block response until it's counted down.
94     private static CountDownLatch sResponseLatch;
95 
96     // Guarded by sLock
97     // Used to request autofill for a autofillable view in AuthenticationActivity
98     private static boolean sRequestAutofill;
99 
100     private Handler mHandler;
101 
102     private EditText mPasswordEditText;
103     private Button mYesButton;
104 
resetStaticState()105     public static void resetStaticState() {
106         setResultCode(null, RESULT_OK);
107         setRequestAutofillForAuthenticationActivity(/* requestAutofill */ false);
108         sDatasets.clear();
109         sResponses.clear();
110         sData = null;
111         sInlineSuggestionsRequest = null;
112         for (int i = 0; i < sPendingIntents.size(); i++) {
113             final PendingIntent pendingIntent = sPendingIntents.get(i);
114             Log.d(TAG, "Cancelling " + pendingIntent);
115             pendingIntent.cancel();
116         }
117     }
118 
119     /**
120      * Creates an {@link IntentSender} with the given unique id for the given dataset.
121      */
createSender(Context context, int id, CannedDataset dataset)122     public static IntentSender createSender(Context context, int id, CannedDataset dataset) {
123         return createSender(context, id, dataset, null);
124     }
125 
createSender(Context context, Intent responseIntent)126     public static IntentSender createSender(Context context, Intent responseIntent) {
127         return createSender(context, null, 1, null, null, responseIntent);
128     }
129 
createSender(Context context, int id, CannedDataset dataset, Bundle outClientState)130     public static IntentSender createSender(Context context, int id,
131             CannedDataset dataset, Bundle outClientState) {
132         return createSender(context, id, dataset, outClientState, null);
133     }
134 
createSender(Context context, int id, CannedDataset dataset, Bundle outClientState, Boolean isEphemeralDataset)135     public static IntentSender createSender(Context context, int id,
136             CannedDataset dataset, Bundle outClientState, Boolean isEphemeralDataset) {
137         Preconditions.checkArgument(id > 0, "id must be positive");
138         Preconditions.checkState(sDatasets.get(id) == null, "already have id");
139         sDatasets.put(id, dataset);
140         return createSender(context, EXTRA_DATASET_ID, id, outClientState, isEphemeralDataset,
141                 null);
142     }
143 
144     /**
145      * Creates an {@link IntentSender} with the given unique id for the given fill response.
146      */
createSender(Context context, int id, CannedFillResponse response)147     public static IntentSender createSender(Context context, int id, CannedFillResponse response) {
148         return createSender(context, id, response, null);
149     }
150 
createSender(Context context, int id, CannedFillResponse response, Bundle outData)151     public static IntentSender createSender(Context context, int id,
152             CannedFillResponse response, Bundle outData) {
153         Preconditions.checkArgument(id > 0, "id must be positive");
154         Preconditions.checkState(sResponses.get(id) == null, "already have id");
155         sResponses.put(id, response);
156         return createSender(context, EXTRA_RESPONSE_ID, id, outData, null, null);
157     }
158 
createSender(Context context, String extraName, int id, Bundle outClientState, Boolean isEphemeralDataset, Intent responseIntent)159     private static IntentSender createSender(Context context, String extraName, int id,
160             Bundle outClientState, Boolean isEphemeralDataset, Intent responseIntent) {
161         Intent intent = new Intent(context, AuthenticationActivity.class);
162         intent.putExtra(extraName, id);
163         if (outClientState != null) {
164             Log.d(TAG, "Create with " + outClientState + " as " + EXTRA_OUTPUT_CLIENT_STATE);
165             intent.putExtra(EXTRA_OUTPUT_CLIENT_STATE, outClientState);
166         }
167         if (isEphemeralDataset != null) {
168             Log.d(TAG, "Create with " + isEphemeralDataset + " as "
169                     + EXTRA_OUTPUT_IS_EPHEMERAL_DATASET);
170             intent.putExtra(EXTRA_OUTPUT_IS_EPHEMERAL_DATASET, isEphemeralDataset);
171         }
172         intent.putExtra(EXTRA_RESPONSE_INTENT, responseIntent);
173         final PendingIntent pendingIntent =
174                 PendingIntent.getActivity(context, id, intent, PendingIntent.FLAG_MUTABLE);
175         sPendingIntents.add(pendingIntent);
176         return pendingIntent.getIntentSender();
177     }
178 
179     /**
180      * Creates an {@link IntentSender} with the given unique id.
181      */
createSender(Context context, int id)182     public static IntentSender createSender(Context context, int id) {
183         Preconditions.checkArgument(id > 0, "id must be positive");
184         return PendingIntent
185                 .getActivity(context, id, new Intent(context, AuthenticationActivity.class),
186                         PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE)
187                 .getIntentSender();
188     }
189 
getData()190     public static Bundle getData() {
191         final Bundle data = sData;
192         sData = null;
193         return data;
194     }
195 
getInlineSuggestionsRequest()196     public static InlineSuggestionsRequest getInlineSuggestionsRequest() {
197         final InlineSuggestionsRequest request = sInlineSuggestionsRequest;
198         sInlineSuggestionsRequest = null;
199         return request;
200     }
201 
202     /**
203      * Sets the value that's passed to {@link Activity#setResult(int, Intent)} when on
204      * {@link Activity#onCreate(Bundle)}.
205      */
setResultCode(int resultCode)206     public static void setResultCode(int resultCode) {
207         synchronized (sLock) {
208             sResultCode = resultCode;
209         }
210     }
211 
212     /**
213      * Sets the value that's passed to {@link Activity#setResult(int, Intent)}, but only calls it
214      * after the {@code latch}'s countdown reaches {@code 0}.
215      */
setResultCode(CountDownLatch latch, int resultCode)216     public static void setResultCode(CountDownLatch latch, int resultCode) {
217         synchronized (sLock) {
218             sResponseLatch = latch;
219             sResultCode = resultCode;
220         }
221     }
222 
setRequestAutofillForAuthenticationActivity(boolean requestAutofill)223     public static void setRequestAutofillForAuthenticationActivity(boolean requestAutofill) {
224         synchronized (sLock) {
225             sRequestAutofill = requestAutofill;
226         }
227     }
228 
229     @Override
onCreate(Bundle savedInstanceState)230     protected void onCreate(Bundle savedInstanceState) {
231         super.onCreate(savedInstanceState);
232 
233         setContentView(R.layout.authentication_activity);
234 
235         mPasswordEditText = findViewById(R.id.password);
236         mYesButton = findViewById(R.id.yes);
237         mYesButton.setOnClickListener(view -> doIt());
238 
239         mHandler = new Handler(Looper.getMainLooper(), (m) -> {
240             switch (m.what) {
241                 case MSG_WAIT_FOR_LATCH:
242                     waitForLatchAndDoIt();
243                     break;
244                 case MSG_REQUEST_AUTOFILL:
245                     requestFocusOnPassword();
246                     break;
247                 default:
248                     throw new IllegalArgumentException("invalid message: " + m);
249             }
250             return true;
251         });
252 
253         if (sResponseLatch != null) {
254             Log.d(TAG, "Delaying message until latch is counted down");
255             mHandler.dispatchMessage(mHandler.obtainMessage(MSG_WAIT_FOR_LATCH));
256         } else if (sRequestAutofill) {
257             mHandler.dispatchMessage(mHandler.obtainMessage(MSG_REQUEST_AUTOFILL));
258         } else {
259             doIt();
260         }
261     }
262 
requestFocusOnPassword()263     private void requestFocusOnPassword() {
264         syncRunOnUiThread(() -> mPasswordEditText.requestFocus());
265     }
266 
waitForLatchAndDoIt()267     private void waitForLatchAndDoIt() {
268         try {
269             final boolean called = sResponseLatch.await(5, TimeUnit.SECONDS);
270             if (!called) {
271                 throw new IllegalStateException("latch not called in 5 seconds");
272             }
273             doIt();
274         } catch (InterruptedException e) {
275             Thread.interrupted();
276             throw new IllegalStateException("interrupted");
277         }
278     }
279 
doIt()280     private void doIt() {
281         final int resultCode;
282         synchronized (sLock) {
283             resultCode = sResultCode;
284         }
285 
286         // If responseIntent is provided, use that to return, otherwise contstruct the response.
287         Intent responseIntent = getIntent().getParcelableExtra(EXTRA_RESPONSE_INTENT, Intent.class);
288         if (responseIntent != null) {
289             Log.d(TAG, "Returning code " + resultCode);
290             setResult(resultCode, responseIntent);
291             finish();
292             return;
293         }
294 
295         // We should get the assist structure...
296         final AssistStructure structure = getIntent().getParcelableExtra(
297                 AutofillManager.EXTRA_ASSIST_STRUCTURE);
298         assertWithMessage("structure not called").that(structure).isNotNull();
299 
300         // and the bundle
301         sData = getIntent().getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE);
302         sInlineSuggestionsRequest = getIntent().getParcelableExtra(
303                 AutofillManager.EXTRA_INLINE_SUGGESTIONS_REQUEST);
304         final CannedFillResponse response =
305                 sResponses.get(getIntent().getIntExtra(EXTRA_RESPONSE_ID, 0));
306         final CannedDataset dataset =
307                 sDatasets.get(getIntent().getIntExtra(EXTRA_DATASET_ID, 0));
308 
309         final Parcelable result;
310 
311         if (response != null) {
312             if (response.getResponseType() == NULL) {
313                 result = null;
314             } else {
315                 result = response.asFillResponse(/* contexts= */ null,
316                         (id) -> Helper.findNodeByResourceId(structure, id));
317             }
318         } else if (dataset != null) {
319             result = dataset.asDataset((id) -> Helper.findNodeByResourceId(structure, id));
320         } else {
321             throw new IllegalStateException("no dataset or response");
322         }
323 
324         // Pass on the auth result
325         final Intent intent = new Intent();
326         intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
327 
328         final Bundle outClientState = getIntent().getBundleExtra(EXTRA_OUTPUT_CLIENT_STATE);
329         if (outClientState != null) {
330             Log.d(TAG, "Adding " + outClientState + " as " + AutofillManager.EXTRA_CLIENT_STATE);
331             intent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, outClientState);
332         }
333         if (getIntent().getExtras().containsKey(EXTRA_OUTPUT_IS_EPHEMERAL_DATASET)) {
334             final boolean isEphemeralDataset = getIntent().getBooleanExtra(
335                     EXTRA_OUTPUT_IS_EPHEMERAL_DATASET, false);
336             Log.d(TAG, "Adding " + isEphemeralDataset + " as "
337                     + AutofillManager.EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET);
338             intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET,
339                     isEphemeralDataset);
340         }
341         Log.d(TAG, "Returning code " + resultCode);
342         setResult(resultCode, intent);
343 
344         // Done
345         finish();
346     }
347 }
348