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