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; 18 19 import static android.autofillservice.cts.CannedFillResponse.ResponseType.NULL; 20 import static android.autofillservice.cts.CannedFillResponse.ResponseType.TIMEOUT; 21 import static android.autofillservice.cts.Helper.dumpStructure; 22 import static android.autofillservice.cts.Helper.getActivityName; 23 import static android.autofillservice.cts.Timeouts.CONNECTION_TIMEOUT; 24 import static android.autofillservice.cts.Timeouts.FILL_EVENTS_TIMEOUT; 25 import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT; 26 import static android.autofillservice.cts.Timeouts.IDLE_UNBIND_TIMEOUT; 27 import static android.autofillservice.cts.Timeouts.SAVE_TIMEOUT; 28 29 import static com.google.common.truth.Truth.assertThat; 30 31 import android.app.assist.AssistStructure; 32 import android.autofillservice.cts.CannedFillResponse.CannedDataset; 33 import android.content.ComponentName; 34 import android.content.IntentSender; 35 import android.os.Bundle; 36 import android.os.CancellationSignal; 37 import android.os.SystemClock; 38 import android.service.autofill.AutofillService; 39 import android.service.autofill.Dataset; 40 import android.service.autofill.FillCallback; 41 import android.service.autofill.FillContext; 42 import android.service.autofill.FillEventHistory; 43 import android.service.autofill.FillEventHistory.Event; 44 import android.service.autofill.FillResponse; 45 import android.service.autofill.SaveCallback; 46 import android.util.Log; 47 48 import androidx.annotation.Nullable; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.concurrent.BlockingQueue; 53 import java.util.concurrent.LinkedBlockingQueue; 54 import java.util.concurrent.TimeUnit; 55 import java.util.concurrent.atomic.AtomicReference; 56 57 /** 58 * Implementation of {@link AutofillService} used in the tests. 59 */ 60 public class InstrumentedAutoFillService extends AutofillService { 61 62 static final String SERVICE_PACKAGE = Helper.MY_PACKAGE; 63 static final String SERVICE_CLASS = "InstrumentedAutoFillService"; 64 65 static final String SERVICE_NAME = SERVICE_PACKAGE + "/." + SERVICE_CLASS; 66 67 private static final String TAG = "InstrumentedAutoFillService"; 68 69 private static final boolean DUMP_FILL_REQUESTS = false; 70 private static final boolean DUMP_SAVE_REQUESTS = false; 71 72 protected static final AtomicReference<InstrumentedAutoFillService> sInstance = 73 new AtomicReference<>(); 74 private static final Replier sReplier = new Replier(); 75 76 private static final Object sLock = new Object(); 77 78 // @GuardedBy("sLock") // NOTE: not using annotation because of dependencies 79 private static boolean sIgnoreUnexpectedRequests = false; 80 81 // @GuardedBy("sLock") // NOTE: not using annotation because of dependencies 82 private static boolean sConnected; 83 84 protected static String sServiceLabel = SERVICE_CLASS; 85 86 public InstrumentedAutoFillService() { 87 sInstance.set(this); 88 sServiceLabel = SERVICE_CLASS; 89 } 90 91 private static InstrumentedAutoFillService peekInstance() { 92 return sInstance.get(); 93 } 94 95 /** 96 * Gets the list of fill events in the {@link FillEventHistory}, waiting until it has the 97 * expected size. 98 */ 99 public static List<Event> getFillEvents(int expectedSize) throws Exception { 100 final List<Event> events = getFillEventHistory(expectedSize).getEvents(); 101 // Sanity check 102 if (expectedSize > 0 && events == null || events.size() != expectedSize) { 103 throw new IllegalStateException("INTERNAL ERROR: events should have " + expectedSize 104 + ", but it is: " + events); 105 } 106 return events; 107 } 108 109 /** 110 * Gets the {@link FillEventHistory}, waiting until it has the expected size. 111 */ 112 public static FillEventHistory getFillEventHistory(int expectedSize) throws Exception { 113 final InstrumentedAutoFillService service = peekInstance(); 114 115 if (expectedSize == 0) { 116 // Need to always sleep as there is no condition / callback to be used to wait until 117 // expected number of events is set. 118 SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms()); 119 final FillEventHistory history = service.getFillEventHistory(); 120 assertThat(history.getEvents()).isNull(); 121 return history; 122 } 123 124 return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> { 125 final FillEventHistory history = service.getFillEventHistory(); 126 if (history == null) { 127 return null; 128 } 129 final List<Event> events = history.getEvents(); 130 if (events != null) { 131 if (events.size() != expectedSize) { 132 Log.v(TAG, "Didn't get " + expectedSize + " events yet: " + events); 133 return null; 134 } 135 } else { 136 Log.v(TAG, "Events is still null (expecting " + expectedSize + ")"); 137 return null; 138 } 139 return history; 140 }); 141 } 142 143 /** 144 * Asserts there is no {@link FillEventHistory}. 145 */ 146 public static void assertNoFillEventHistory() { 147 // Need to always sleep as there is no condition / callback to be used to wait until 148 // expected number of events is set. 149 SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms()); 150 assertThat(peekInstance().getFillEventHistory()).isNull(); 151 152 } 153 154 /** 155 * Gets the service label associated with the current instance. 156 */ 157 public static String getServiceLabel() { 158 return sServiceLabel; 159 } 160 161 @Override 162 public void onConnected() { 163 synchronized (sLock) { 164 Log.v(TAG, "onConnected(): connected=" + sConnected); 165 sConnected = true; 166 } 167 } 168 169 @Override 170 public void onDisconnected() { 171 synchronized (sLock) { 172 Log.v(TAG, "onDisconnected(): connected=" + sConnected); 173 sConnected = false; 174 } 175 } 176 177 @Override 178 public void onFillRequest(android.service.autofill.FillRequest request, 179 CancellationSignal cancellationSignal, FillCallback callback) { 180 if (DUMP_FILL_REQUESTS) dumpStructure("onFillRequest()", request.getFillContexts()); 181 synchronized (sLock) { 182 if (sIgnoreUnexpectedRequests || !fromSamePackage(request.getFillContexts())) { 183 Log.w(TAG, "Ignoring onFillRequest()"); 184 return; 185 } 186 } 187 sReplier.onFillRequest(request.getFillContexts(), request.getClientState(), 188 cancellationSignal, callback, request.getFlags()); 189 } 190 191 @Override 192 public void onSaveRequest(android.service.autofill.SaveRequest request, 193 SaveCallback callback) { 194 if (DUMP_SAVE_REQUESTS) dumpStructure("onSaveRequest()", request.getFillContexts()); 195 synchronized (sLock) { 196 if (sIgnoreUnexpectedRequests || !fromSamePackage(request.getFillContexts())) { 197 Log.w(TAG, "Ignoring onSaveRequest()"); 198 return; 199 } 200 } 201 sReplier.onSaveRequest(request.getFillContexts(), request.getClientState(), callback, 202 request.getDatasetIds()); 203 } 204 205 private static boolean isConnected() { 206 synchronized (sLock) { 207 return sConnected; 208 } 209 } 210 211 private boolean fromSamePackage(List<FillContext> contexts) { 212 final ComponentName component = contexts.get(contexts.size() - 1).getStructure() 213 .getActivityComponent(); 214 final String actualPackage = component.getPackageName(); 215 if (!actualPackage.equals(getPackageName()) 216 && !actualPackage.equals(sReplier.mAcceptedPackageName)) { 217 Log.w(TAG, "Got request from package " + actualPackage); 218 return false; 219 } 220 return true; 221 } 222 223 /** 224 * Sets whether unexpected calls to 225 * {@link #onFillRequest(android.service.autofill.FillRequest, CancellationSignal, FillCallback)} 226 * should throw an exception. 227 */ 228 public static void setIgnoreUnexpectedRequests(boolean ignore) { 229 synchronized (sLock) { 230 sIgnoreUnexpectedRequests = ignore; 231 } 232 } 233 234 /** 235 * Waits until {@link #onConnected()} is called, or fails if it times out. 236 * 237 * <p>This method is useful on tests that explicitly verifies the connection, but should be 238 * avoided in other tests, as it adds extra time to the test execution (and flakiness in cases 239 * where the service might have being disconnected already; for example, if the fill request 240 * was replied with a {@code null} response) - if a text needs to block until the service 241 * receives a callback, it should use {@link Replier#getNextFillRequest()} instead. 242 */ 243 static void waitUntilConnected() throws Exception { 244 waitConnectionState(CONNECTION_TIMEOUT, true); 245 } 246 247 /** 248 * Waits until {@link #onDisconnected()} is called, or fails if it times out. 249 * 250 * <p>This method is useful on tests that explicitly verifies the connection, but should be 251 * avoided in other tests, as it adds extra time to the test execution. 252 */ 253 static void waitUntilDisconnected() throws Exception { 254 waitConnectionState(IDLE_UNBIND_TIMEOUT, false); 255 } 256 257 private static void waitConnectionState(Timeout timeout, boolean expected) throws Exception { 258 timeout.run("wait for connected=" + expected, () -> { 259 return isConnected() == expected ? Boolean.TRUE : null; 260 }); 261 } 262 263 /** 264 * Gets the {@link Replier} singleton. 265 */ 266 static Replier getReplier() { 267 return sReplier; 268 } 269 270 static void resetStaticState() { 271 sInstance.set(null); 272 sConnected = false; 273 sServiceLabel = SERVICE_CLASS; 274 } 275 276 /** 277 * POJO representation of the contents of a 278 * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest, 279 * CancellationSignal, FillCallback)} that can be asserted at the end of a test case. 280 */ 281 static final class FillRequest { 282 final AssistStructure structure; 283 final List<FillContext> contexts; 284 final Bundle data; 285 final CancellationSignal cancellationSignal; 286 final FillCallback callback; 287 final int flags; 288 289 private FillRequest(List<FillContext> contexts, Bundle data, 290 CancellationSignal cancellationSignal, FillCallback callback, int flags) { 291 this.contexts = contexts; 292 this.data = data; 293 this.cancellationSignal = cancellationSignal; 294 this.callback = callback; 295 this.flags = flags; 296 structure = contexts.get(contexts.size() - 1).getStructure(); 297 } 298 299 @Override 300 public String toString() { 301 return "FillRequest:" + getActivityName(contexts); 302 } 303 } 304 305 /** 306 * POJO representation of the contents of a 307 * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)} 308 * that can be asserted at the end of a test case. 309 */ 310 static final class SaveRequest { 311 public final List<FillContext> contexts; 312 public final AssistStructure structure; 313 public final Bundle data; 314 public final SaveCallback callback; 315 public final List<String> datasetIds; 316 317 private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback, 318 List<String> datasetIds) { 319 if (contexts != null && contexts.size() > 0) { 320 structure = contexts.get(contexts.size() - 1).getStructure(); 321 } else { 322 structure = null; 323 } 324 this.contexts = contexts; 325 this.data = data; 326 this.callback = callback; 327 this.datasetIds = datasetIds; 328 } 329 330 @Override 331 public String toString() { 332 return "SaveRequest:" + getActivityName(contexts); 333 } 334 } 335 336 /** 337 * Object used to answer a 338 * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest, 339 * CancellationSignal, FillCallback)} 340 * on behalf of a unit test method. 341 */ 342 static final class Replier { 343 344 private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>(); 345 private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>(); 346 private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>(); 347 348 private List<Throwable> mExceptions; 349 private IntentSender mOnSaveIntentSender; 350 private String mAcceptedPackageName; 351 352 private boolean mReportUnhandledFillRequest = true; 353 private boolean mReportUnhandledSaveRequest = true; 354 355 private Replier() { 356 } 357 358 private IdMode mIdMode = IdMode.RESOURCE_ID; 359 360 public void setIdMode(IdMode mode) { 361 this.mIdMode = mode; 362 } 363 364 public void acceptRequestsFromPackage(String packageName) { 365 mAcceptedPackageName = packageName; 366 } 367 368 /** 369 * Gets the exceptions thrown asynchronously, if any. 370 */ 371 @Nullable List<Throwable> getExceptions() { 372 return mExceptions; 373 } 374 375 private void addException(@Nullable Throwable e) { 376 if (e == null) return; 377 378 if (mExceptions == null) { 379 mExceptions = new ArrayList<>(); 380 } 381 mExceptions.add(e); 382 } 383 384 /** 385 * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just 386 * one {@link Dataset}. 387 */ 388 Replier addResponse(CannedDataset dataset) { 389 return addResponse(new CannedFillResponse.Builder() 390 .addDataset(dataset) 391 .build()); 392 } 393 394 /** 395 * Sets the expectation for the next {@code onFillRequest}. 396 */ 397 Replier addResponse(CannedFillResponse response) { 398 if (response == null) { 399 throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead"); 400 } 401 mResponses.add(response); 402 return this; 403 } 404 405 /** 406 * Sets the {@link IntentSender} that is passed to 407 * {@link SaveCallback#onSuccess(IntentSender)}. 408 */ 409 void setOnSave(IntentSender intentSender) { 410 mOnSaveIntentSender = intentSender; 411 } 412 413 /** 414 * Gets the next fill request, in the order received. 415 * 416 * <p>Typically called at the end of a test case, to assert the initial request. 417 */ 418 FillRequest getNextFillRequest() { 419 FillRequest request; 420 try { 421 request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS); 422 } catch (InterruptedException e) { 423 Thread.currentThread().interrupt(); 424 throw new IllegalStateException("Interrupted", e); 425 } 426 if (request == null) { 427 throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called"); 428 } 429 return request; 430 } 431 432 /** 433 * Asserts that {@link #onFillRequest(List, Bundle, CancellationSignal, FillCallback, int)} 434 * was not called. 435 * 436 * <p>Should only be called in cases where it's not expected to be called, as it will 437 * sleep for a few ms. 438 */ 439 void assertOnFillRequestNotCalled() { 440 SystemClock.sleep(FILL_TIMEOUT.getMaxValue()); 441 assertThat(mFillRequests).isEmpty(); 442 } 443 444 /** 445 * Asserts all {@link AutofillService#onFillRequest( 446 * android.service.autofill.FillRequest, CancellationSignal, FillCallback) fill requests} 447 * received by the service were properly {@link #getNextFillRequest() handled} by the test 448 * case. 449 */ 450 void assertNoUnhandledFillRequests() { 451 if (mFillRequests.isEmpty()) return; // Good job, test case! 452 453 if (!mReportUnhandledFillRequest) { 454 // Just log, so it's not thrown again on @After if already thrown on main body 455 Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, " 456 + "but logging just in case: " + mFillRequests); 457 return; 458 } 459 460 mReportUnhandledFillRequest = false; 461 throw new AssertionError(mFillRequests.size() + " unhandled fill requests: " 462 + mFillRequests); 463 } 464 465 /** 466 * Gets the current number of unhandled requests. 467 */ 468 int getNumberUnhandledFillRequests() { 469 return mFillRequests.size(); 470 } 471 472 /** 473 * Gets the next save request, in the order received. 474 * 475 * <p>Typically called at the end of a test case, to assert the initial request. 476 */ 477 SaveRequest getNextSaveRequest() { 478 SaveRequest request; 479 try { 480 request = mSaveRequests.poll(SAVE_TIMEOUT.ms(), TimeUnit.MILLISECONDS); 481 } catch (InterruptedException e) { 482 Thread.currentThread().interrupt(); 483 throw new IllegalStateException("Interrupted", e); 484 } 485 if (request == null) { 486 throw new RetryableException(SAVE_TIMEOUT, "onSaveRequest() not called"); 487 } 488 return request; 489 } 490 491 /** 492 * Asserts all 493 * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback) 494 * save requests} received by the service were properly 495 * {@link #getNextFillRequest() handled} by the test case. 496 */ 497 void assertNoUnhandledSaveRequests() { 498 if (mSaveRequests.isEmpty()) return; // Good job, test case! 499 500 if (!mReportUnhandledSaveRequest) { 501 // Just log, so it's not thrown again on @After if already thrown on main body 502 Log.d(TAG, "assertNoUnhandledSaveRequests(): already reported, " 503 + "but logging just in case: " + mSaveRequests); 504 return; 505 } 506 507 mReportUnhandledSaveRequest = false; 508 throw new AssertionError(mSaveRequests.size() + " unhandled save requests: " 509 + mSaveRequests); 510 } 511 512 /** 513 * Resets its internal state. 514 */ 515 void reset() { 516 mResponses.clear(); 517 mFillRequests.clear(); 518 mSaveRequests.clear(); 519 mExceptions = null; 520 mOnSaveIntentSender = null; 521 mAcceptedPackageName = null; 522 mReportUnhandledFillRequest = true; 523 mReportUnhandledSaveRequest = true; 524 } 525 526 private void onFillRequest(List<FillContext> contexts, Bundle data, 527 CancellationSignal cancellationSignal, FillCallback callback, int flags) { 528 try { 529 CannedFillResponse response = null; 530 try { 531 response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS); 532 } catch (InterruptedException e) { 533 Log.w(TAG, "Interrupted getting CannedResponse: " + e); 534 Thread.currentThread().interrupt(); 535 addException(e); 536 return; 537 } 538 if (response == null) { 539 final String activityName = getActivityName(contexts); 540 final String msg = "onFillRequest() for activity " + activityName 541 + " received when no canned response was set."; 542 dumpStructure(msg, contexts); 543 return; 544 } 545 if (response.getResponseType() == NULL) { 546 Log.d(TAG, "onFillRequest(): replying with null"); 547 callback.onSuccess(null); 548 return; 549 } 550 551 if (response.getResponseType() == TIMEOUT) { 552 Log.d(TAG, "onFillRequest(): not replying at all"); 553 return; 554 } 555 556 final String failureMessage = response.getFailureMessage(); 557 if (failureMessage != null) { 558 Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage); 559 callback.onFailure(failureMessage); 560 return; 561 } 562 563 final FillResponse fillResponse; 564 565 switch (mIdMode) { 566 case RESOURCE_ID: 567 fillResponse = response.asFillResponse( 568 (id) -> Helper.findNodeByResourceId(contexts, id)); 569 break; 570 case HTML_NAME: 571 fillResponse = response.asFillResponse( 572 (name) -> Helper.findNodeByHtmlName(contexts, name)); 573 break; 574 case HTML_NAME_OR_RESOURCE_ID: 575 fillResponse = response.asFillResponse( 576 (id) -> Helper.findNodeByHtmlNameOrResourceId(contexts, id)); 577 break; 578 default: 579 throw new IllegalStateException("Unknown id mode: " + mIdMode); 580 } 581 582 Log.v(TAG, "onFillRequest(): fillResponse = " + fillResponse); 583 callback.onSuccess(fillResponse); 584 } catch (Throwable t) { 585 addException(t); 586 } finally { 587 mFillRequests.offer(new FillRequest(contexts, data, cancellationSignal, callback, 588 flags)); 589 } 590 } 591 592 private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback, 593 List<String> datasetIds) { 594 Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender); 595 mSaveRequests.offer(new SaveRequest(contexts, data, callback, datasetIds)); 596 if (mOnSaveIntentSender != null) { 597 callback.onSuccess(mOnSaveIntentSender); 598 } else { 599 callback.onSuccess(); 600 } 601 } 602 } 603 } 604