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