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