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.testcore;
18 
19 import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.FAILURE;
20 import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.NULL;
21 import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.TIMEOUT;
22 import static android.autofillservice.cts.testcore.Helper.dumpStructure;
23 import static android.autofillservice.cts.testcore.Helper.getActivityName;
24 import static android.autofillservice.cts.testcore.Timeouts.CONNECTION_TIMEOUT;
25 import static android.autofillservice.cts.testcore.Timeouts.FILL_EVENTS_TIMEOUT;
26 import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
27 import static android.autofillservice.cts.testcore.Timeouts.IDLE_UNBIND_TIMEOUT;
28 import static android.autofillservice.cts.testcore.Timeouts.RESPONSE_DELAY_MS;
29 import static android.autofillservice.cts.testcore.Timeouts.SAVE_TIMEOUT;
30 
31 import static com.google.common.truth.Truth.assertThat;
32 
33 import android.app.assist.AssistStructure;
34 import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
35 import android.autofillservice.cts.testcore.CannedFillResponse.ResponseType;
36 import android.content.ComponentName;
37 import android.content.IntentSender;
38 import android.os.Bundle;
39 import android.os.CancellationSignal;
40 import android.os.Handler;
41 import android.os.HandlerThread;
42 import android.os.SystemClock;
43 import android.service.autofill.AutofillService;
44 import android.service.autofill.Dataset;
45 import android.service.autofill.FillCallback;
46 import android.service.autofill.FillContext;
47 import android.service.autofill.FillEventHistory;
48 import android.service.autofill.FillEventHistory.Event;
49 import android.service.autofill.FillResponse;
50 import android.service.autofill.SaveCallback;
51 import android.service.autofill.SavedDatasetsInfoCallback;
52 import android.util.Log;
53 import android.view.inputmethod.InlineSuggestionsRequest;
54 
55 import androidx.annotation.NonNull;
56 import androidx.annotation.Nullable;
57 
58 import com.android.compatibility.common.util.RetryableException;
59 import com.android.compatibility.common.util.TestNameUtils;
60 import com.android.compatibility.common.util.Timeout;
61 
62 import java.io.FileDescriptor;
63 import java.io.IOException;
64 import java.io.PrintWriter;
65 import java.io.StringWriter;
66 import java.util.ArrayList;
67 import java.util.List;
68 import java.util.concurrent.BlockingQueue;
69 import java.util.concurrent.LinkedBlockingQueue;
70 import java.util.concurrent.TimeUnit;
71 import java.util.concurrent.atomic.AtomicBoolean;
72 import java.util.concurrent.atomic.AtomicReference;
73 import java.util.function.Consumer;
74 import java.util.function.Function;
75 
76 /**
77  * Implementation of {@link AutofillService} used in the tests.
78  */
79 public class InstrumentedAutoFillService extends AutofillService {
80 
81     public static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
82     public static final String SERVICE_CLASS = "InstrumentedAutoFillService";
83 
84     public static final String SERVICE_NAME = SERVICE_PACKAGE + "/.testcore." + SERVICE_CLASS;
85 
86     public static String sServiceLabel = SERVICE_CLASS;
87 
88     // TODO(b/125844305): remove once fixed
89     private static final boolean FAIL_ON_INVALID_CONNECTION_STATE = false;
90 
91     private static final String TAG = "InstrumentedAutoFillService";
92 
93     private static final boolean DUMP_FILL_REQUESTS = false;
94     private static final boolean DUMP_SAVE_REQUESTS = false;
95 
96     protected static final AtomicReference<InstrumentedAutoFillService> sInstance =
97             new AtomicReference<>();
98     private static final Replier sReplier = new Replier();
99     @Nullable
100     private static Consumer<SavedDatasetsInfoCallback> sSavedDatasetsInfoReplier;
101 
102     private static AtomicBoolean sConnected = new AtomicBoolean(false);
103 
104     // We must handle all requests in a separate thread as the service's main thread is the also
105     // the UI thread of the test process and we don't want to hose it in case of failures here
106     private static final HandlerThread sMyThread = new HandlerThread("MyServiceThread");
107     private final Handler mHandler;
108 
109     private boolean mConnected;
110 
111     static {
Log.i(TAG, "Starting thread " + sMyThread)112         Log.i(TAG, "Starting thread " + sMyThread);
sMyThread.start()113         sMyThread.start();
114     }
115 
InstrumentedAutoFillService()116     public InstrumentedAutoFillService() {
117         sInstance.set(this);
118         sServiceLabel = SERVICE_CLASS;
119         mHandler = Handler.createAsync(sMyThread.getLooper());
120         sReplier.setHandler(mHandler);
121     }
122 
peekInstance()123     private static InstrumentedAutoFillService peekInstance() {
124         return sInstance.get();
125     }
126 
127     /**
128      * Gets the list of fill events in the {@link FillEventHistory}, waiting until it has the
129      * expected size.
130      */
getFillEvents(int expectedSize)131     public static List<Event> getFillEvents(int expectedSize) throws Exception {
132         final List<Event> events = getFillEventHistory(expectedSize).getEvents();
133         // Validation check
134         if (expectedSize > 0 && events == null || events.size() != expectedSize) {
135             throw new IllegalStateException("INTERNAL ERROR: events should have " + expectedSize
136                     + ", but it is: " + events);
137         }
138         return events;
139     }
140 
141     /**
142      * Gets the {@link FillEventHistory}, waiting until it has the expected size.
143      */
getFillEventHistory(int expectedSize)144     public static FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
145         final InstrumentedAutoFillService service = peekInstance();
146 
147         if (expectedSize == 0) {
148             // Need to always sleep as there is no condition / callback to be used to wait until
149             // expected number of events is set.
150             SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
151             final FillEventHistory history = service.getFillEventHistory();
152             assertThat(history.getEvents()).isNull();
153             return history;
154         }
155 
156         return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
157             final FillEventHistory history = service.getFillEventHistory();
158             if (history == null) {
159                 return null;
160             }
161             final List<Event> events = history.getEvents();
162             if (events != null) {
163                 if (events.size() != expectedSize) {
164                     Log.v(TAG, "Didn't get " + expectedSize + " events yet: " + events);
165                     return null;
166                 }
167             } else {
168                 Log.v(TAG, "Events is still null (expecting " + expectedSize + ")");
169                 return null;
170             }
171             return history;
172         });
173     }
174 
175     /**
176      * Asserts there is no {@link FillEventHistory}.
177      */
assertNoFillEventHistory()178     public static void assertNoFillEventHistory() {
179         // Need to always sleep as there is no condition / callback to be used to wait until
180         // expected number of events is set.
181         SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
182         assertThat(peekInstance().getFillEventHistory()).isNull();
183 
184     }
185 
186     /**
187      * Gets the service label associated with the current instance.
188      */
getServiceLabel()189     public static String getServiceLabel() {
190         return sServiceLabel;
191     }
192 
handleConnected(boolean connected)193     private void handleConnected(boolean connected) {
194         Log.v(TAG, "handleConnected(): from " + sConnected.get() + " to " + connected);
195         sConnected.set(connected);
196     }
197 
198     @Override
onSessionDestroyed(@ullable FillEventHistory history)199     public void onSessionDestroyed(@Nullable FillEventHistory history) {
200         Log.v(TAG, "onSessionDestroyed() called");
201         mHandler.post(() -> sReplier.addLastFillEventHistory(history));
202     }
203 
204     @Override
onConnected()205     public void onConnected() {
206         Log.v(TAG, "onConnected");
207         if (mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
208             dumpSelf();
209             sReplier.addException(new IllegalStateException("onConnected() called again"));
210         }
211         mConnected = true;
212         mHandler.post(() -> handleConnected(true));
213     }
214 
215     @Override
onDisconnected()216     public void onDisconnected() {
217         Log.v(TAG, "onDisconnected");
218         if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
219             dumpSelf();
220             sReplier.addException(
221                     new IllegalStateException("onDisconnected() called when disconnected"));
222         }
223         mConnected = false;
224         mHandler.post(() -> handleConnected(false));
225     }
226 
227     @Override
onFillRequest(android.service.autofill.FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)228     public void onFillRequest(android.service.autofill.FillRequest request,
229             CancellationSignal cancellationSignal, FillCallback callback) {
230         final ComponentName component = getLastActivityComponent(request.getFillContexts());
231         if (DUMP_FILL_REQUESTS) {
232             dumpStructure("onFillRequest()", request.getFillContexts());
233         } else {
234             Log.i(TAG, "onFillRequest() for " + component.toShortString());
235         }
236         if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
237             dumpSelf();
238             sReplier.addException(
239                     new IllegalStateException("onFillRequest() called when disconnected"));
240         }
241 
242         if (!TestNameUtils.isRunningTest()) {
243             Log.e(TAG, "onFillRequest(" + component + ") called after tests finished");
244             return;
245         }
246         if (!fromSamePackage(component))  {
247             Log.w(TAG, "Ignoring onFillRequest() from different package: " + component);
248             return;
249         }
250         mHandler.post(
251                 () -> sReplier.onFillRequest(request.getFillContexts(), request.getHints(),
252                         request.getClientState(),
253                         cancellationSignal, callback, request.getFlags(),
254                         request.getInlineSuggestionsRequest(),
255                         request.getDelayedFillIntentSender(),
256                         request.getId()));
257     }
258 
259     @Override
onSaveRequest(android.service.autofill.SaveRequest request, SaveCallback callback)260     public void onSaveRequest(android.service.autofill.SaveRequest request,
261             SaveCallback callback) {
262         if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
263             dumpSelf();
264             sReplier.addException(
265                     new IllegalStateException("onSaveRequest() called when disconnected"));
266         }
267         mHandler.post(()->handleSaveRequest(request, callback));
268     }
269 
handleSaveRequest(android.service.autofill.SaveRequest request, SaveCallback callback)270     private void handleSaveRequest(android.service.autofill.SaveRequest request,
271             SaveCallback callback) {
272         final ComponentName component = getLastActivityComponent(request.getFillContexts());
273         if (!TestNameUtils.isRunningTest()) {
274             Log.e(TAG, "onSaveRequest(" + component + ") called after tests finished");
275             return;
276         }
277         if (!fromSamePackage(component)) {
278             Log.w(TAG, "Ignoring onSaveRequest() from different package: " + component);
279             return;
280         }
281         if (DUMP_SAVE_REQUESTS) {
282             dumpStructure("onSaveRequest()", request.getFillContexts());
283         } else {
284             Log.i(TAG, "onSaveRequest() for " + component.toShortString());
285         }
286         mHandler.post(() -> sReplier.onSaveRequest(request.getFillContexts(),
287                 request.getClientState(), callback,
288                 request.getDatasetIds()));
289     }
290 
291     @Override
onSavedDatasetsInfoRequest(@onNull SavedDatasetsInfoCallback callback)292     public void onSavedDatasetsInfoRequest(@NonNull SavedDatasetsInfoCallback callback) {
293         if (sSavedDatasetsInfoReplier == null) {
294             super.onSavedDatasetsInfoRequest(callback);
295         } else {
296             sSavedDatasetsInfoReplier.accept(callback);
297         }
298     }
299 
setSavedDatasetsInfoReplier( @ullable Consumer<SavedDatasetsInfoCallback> savedDatasetsInfoReplier)300     public static void setSavedDatasetsInfoReplier(
301             @Nullable Consumer<SavedDatasetsInfoCallback> savedDatasetsInfoReplier) {
302         sSavedDatasetsInfoReplier = savedDatasetsInfoReplier;
303     }
304 
isConnected()305     public static boolean isConnected() {
306         return sConnected.get();
307     }
308 
fromSamePackage(ComponentName component)309     private boolean fromSamePackage(ComponentName component) {
310         final String actualPackage = component.getPackageName();
311         if (!actualPackage.equals(getPackageName())
312                 && !actualPackage.equals(sReplier.mAcceptedPackageName)) {
313             Log.w(TAG, "Got request from package " + actualPackage);
314             return false;
315         }
316         return true;
317     }
318 
getLastActivityComponent(List<FillContext> contexts)319     private ComponentName getLastActivityComponent(List<FillContext> contexts) {
320         return contexts.get(contexts.size() - 1).getStructure().getActivityComponent();
321     }
322 
dumpSelf()323     private void dumpSelf()  {
324         try {
325             try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
326                 dump(null, pw, null);
327                 pw.flush();
328                 final String dump = sw.toString();
329                 Log.e(TAG, "dumpSelf(): " + dump);
330             }
331         } catch (IOException e) {
332             Log.e(TAG, "I don't always fail to dump, but when I do, I dump the failure", e);
333         }
334     }
335 
336     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)337     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
338         pw.print("sConnected: "); pw.println(sConnected);
339         pw.print("mConnected: "); pw.println(mConnected);
340         pw.print("sInstance: "); pw.println(sInstance);
341         pw.println("sReplier: "); sReplier.dump(pw);
342     }
343 
344     /**
345      * Waits until {@link #onConnected()} is called, or fails if it times out.
346      *
347      * <p>This method is useful on tests that explicitly verifies the connection, but should be
348      * avoided in other tests, as it adds extra time to the test execution (and flakiness in cases
349      * where the service might have being disconnected already; for example, if the fill request
350      * was replied with a {@code null} response) - if a text needs to block until the service
351      * receives a callback, it should use {@link Replier#getNextFillRequest()} instead.
352      */
waitUntilConnected()353     public static void waitUntilConnected() throws Exception {
354         waitConnectionState(CONNECTION_TIMEOUT, true);
355     }
356 
357     /**
358      * Waits until {@link #onDisconnected()} is called, or fails if it times out.
359      *
360      * <p>This method is useful on tests that explicitly verifies the connection, but should be
361      * avoided in other tests, as it adds extra time to the test execution.
362      */
waitUntilDisconnected()363     public static void waitUntilDisconnected() throws Exception {
364         waitConnectionState(IDLE_UNBIND_TIMEOUT, false);
365     }
366 
waitConnectionState(Timeout timeout, boolean expected)367     private static void waitConnectionState(Timeout timeout, boolean expected) throws Exception {
368         timeout.run("wait for connected=" + expected,  () -> {
369             return isConnected() == expected ? Boolean.TRUE : null;
370         });
371     }
372 
373     /**
374      * Gets the {@link Replier} singleton.
375      */
getReplier()376     public static Replier getReplier() {
377         return sReplier;
378     }
379 
resetStaticState()380     public static void resetStaticState() {
381         sInstance.set(null);
382         sConnected.set(false);
383         sServiceLabel = SERVICE_CLASS;
384         sSavedDatasetsInfoReplier = null;
385     }
386 
387     /**
388      * POJO representation of the contents of a
389      * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
390      * CancellationSignal, FillCallback)} that can be asserted at the end of a test case.
391      */
392     public static final class FillRequest {
393         public final AssistStructure structure;
394         public final List<FillContext> contexts;
395         public final Bundle data;
396         public final CancellationSignal cancellationSignal;
397         public final FillCallback callback;
398         public final int flags;
399         public final InlineSuggestionsRequest inlineRequest;
400         public final IntentSender delayFillIntentSender;
401         public final int requestId;
402         public final List<String> hints;
403 
FillRequest(List<FillContext> contexts, List<String> hints, Bundle data, CancellationSignal cancellationSignal, FillCallback callback, int flags, InlineSuggestionsRequest inlineRequest, IntentSender delayFillIntentSender, int requestId)404         private FillRequest(List<FillContext> contexts, List<String> hints, Bundle data,
405                 CancellationSignal cancellationSignal, FillCallback callback, int flags,
406                 InlineSuggestionsRequest inlineRequest,
407                 IntentSender delayFillIntentSender,
408                 int requestId) {
409             this.contexts = contexts;
410             this.hints = hints;
411             this.data = data;
412             this.cancellationSignal = cancellationSignal;
413             this.callback = callback;
414             this.flags = flags;
415             this.structure = contexts.get(contexts.size() - 1).getStructure();
416             this.inlineRequest = inlineRequest;
417             this.delayFillIntentSender = delayFillIntentSender;
418             this.requestId = requestId;
419         }
420 
421         @Override
toString()422         public String toString() {
423             return "FillRequest[activity=" + getActivityName(contexts) + ", flags=" + flags
424                     + ", bundle=" + data + ", structure=" + Helper.toString(structure) + "]";
425         }
426     }
427 
428     /**
429      * POJO representation of the contents of a
430      * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)}
431      * that can be asserted at the end of a test case.
432      */
433     public static final class SaveRequest {
434         public final List<FillContext> contexts;
435         public final AssistStructure structure;
436         public final Bundle data;
437         public final SaveCallback callback;
438         public final List<String> datasetIds;
439 
SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback, List<String> datasetIds)440         private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
441                 List<String> datasetIds) {
442             if (contexts != null && contexts.size() > 0) {
443                 structure = contexts.get(contexts.size() - 1).getStructure();
444             } else {
445                 structure = null;
446             }
447             this.contexts = contexts;
448             this.data = data;
449             this.callback = callback;
450             this.datasetIds = datasetIds;
451         }
452 
453         @Override
toString()454         public String toString() {
455             return "SaveRequest:" + getActivityName(contexts);
456         }
457     }
458 
459     /**
460      * Object used to answer a
461      * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
462      * CancellationSignal, FillCallback)}
463      * on behalf of a unit test method.
464      */
465     public static final class Replier {
466 
467         /*
468          * This is used by the test to skip explicitly calling sReplier.getFillRequest().
469          * This will store a callback that gets called whenever a new Fill Request comes.
470          * This makes the code simpler, as only one step is needed to send FillResponse
471          * and verify FillRequest.
472          *
473          * Before:
474          *   sReplier.addResponse(...)
475          *   triggerFillRequestMechanism()
476          *   fr = sReplier.getFillRequest()
477          *   assert(fr)...isTrue()
478          *
479          * After:
480          *   sReplier.onRequest((fr) -> {
481          *      assert(fr)...isTrue()
482          *      return new FillResponse()
483          *   })
484          *   triggerFillRequestMechanism()
485          **/
486         private final BlockingQueue<Function<FillRequest, CannedFillResponse>> mLazyResponses =
487                 new LinkedBlockingQueue<>();
488         private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>();
489         private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>();
490         private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>();
491 
492         private List<Throwable> mExceptions;
493         private IntentSender mOnSaveIntentSender;
494         private String mAcceptedPackageName;
495 
496         private Handler mHandler;
497 
498         private boolean mReportUnhandledFillRequest = true;
499         private boolean mReportUnhandledSaveRequest = true;
500 
501         private @Nullable FillEventHistory mFillEventHistory = null;
502         private int mSessionDestroyedCount = 0;
503 
Replier()504         private Replier() {
505         }
506 
507         private IdMode mIdMode = IdMode.RESOURCE_ID;
508 
setIdMode(IdMode mode)509         public void setIdMode(IdMode mode) {
510             this.mIdMode = mode;
511         }
512 
acceptRequestsFromPackage(String packageName)513         public void acceptRequestsFromPackage(String packageName) {
514             mAcceptedPackageName = packageName;
515         }
516 
517         /**
518          * Gets the exceptions thrown asynchronously, if any.
519          */
520         @Nullable
getExceptions()521         public List<Throwable> getExceptions() {
522             return mExceptions;
523         }
524 
addException(@ullable Throwable e)525         private void addException(@Nullable Throwable e) {
526             if (e == null) return;
527 
528             if (mExceptions == null) {
529                 mExceptions = new ArrayList<>();
530             }
531             mExceptions.add(e);
532         }
533 
534         /**
535          * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just
536          * one {@link Dataset}.
537          */
addResponse(CannedDataset dataset)538         public Replier addResponse(CannedDataset dataset) {
539             return addResponse(new CannedFillResponse.Builder()
540                     .addDataset(dataset)
541                     .build());
542         }
543 
544         /**
545          * Sets the expectation for the next {@code onFillRequest}.
546          */
addResponse(CannedFillResponse response)547         public Replier addResponse(CannedFillResponse response) {
548             if (response == null) {
549                 throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
550             }
551             mResponses.add(response);
552             return this;
553         }
554 
onRequest(Function<FillRequest, CannedFillResponse> lazyResponse)555         public Replier onRequest(Function<FillRequest, CannedFillResponse> lazyResponse) {
556             if (lazyResponse == null) {
557                 throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
558             }
559             mLazyResponses.add(lazyResponse);
560             return this;
561         }
562 
563         /**
564          * Sets the {@link IntentSender} that is passed to
565          * {@link SaveCallback#onSuccess(IntentSender)}.
566          */
setOnSave(IntentSender intentSender)567         public Replier setOnSave(IntentSender intentSender) {
568             mOnSaveIntentSender = intentSender;
569             return this;
570         }
571 
572         /**
573          * Gets the next fill request, in the order received.
574          */
getNextFillRequest()575         public FillRequest getNextFillRequest() {
576             FillRequest request;
577             try {
578                 request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
579             } catch (InterruptedException e) {
580                 Thread.currentThread().interrupt();
581                 throw new IllegalStateException("Interrupted", e);
582             }
583             if (request == null) {
584                 throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called");
585             }
586             return request;
587         }
588 
589         /**
590          * Gets the last FillEventHistory that was return as part of onSessionDestroyed(), for easy
591          * assertion
592          */
getLastFillEventHistory()593         public @Nullable FillEventHistory getLastFillEventHistory() {
594             return mFillEventHistory;
595         }
596 
597         /**
598          * Used by InstrumentedAutofillService to add the last FillEventHistory returned by
599          * onSessionDestroyed()
600          */
addLastFillEventHistory(@ullable FillEventHistory history)601         public void addLastFillEventHistory(@Nullable FillEventHistory history) {
602             mFillEventHistory = history;
603             mSessionDestroyedCount += 1;
604         }
605 
606         /**
607          *
608          * @return the amount of onSessionDestroyed() calls the service has received.
609          */
getSessionDestroyedCount()610         public int getSessionDestroyedCount() {
611             return mSessionDestroyedCount;
612         }
613 
614         /**
615          * Asserts that {@link #onFillRequest(List, Bundle, CancellationSignal, FillCallback, int)}
616          * was not called.
617          *
618          * <p>Should only be called in cases where it's not expected to be called, as it will
619          * sleep for a few ms.
620          */
assertOnFillRequestNotCalled()621         public void assertOnFillRequestNotCalled() {
622             SystemClock.sleep(FILL_TIMEOUT.getMaxValue());
623             assertThat(mFillRequests).isEmpty();
624         }
625 
626         /**
627          * Asserts all {@link AutofillService#onFillRequest(
628          * android.service.autofill.FillRequest,  CancellationSignal, FillCallback) fill requests}
629          * received by the service were properly {@link #getNextFillRequest() handled} by the test
630          * case.
631          */
assertNoUnhandledFillRequests()632         public void assertNoUnhandledFillRequests() {
633             if (mFillRequests.isEmpty()) return; // Good job, test case!
634 
635             if (!mReportUnhandledFillRequest) {
636                 // Just log, so it's not thrown again on @After if already thrown on main body
637                 Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
638                         + "but logging just in case: " + mFillRequests);
639                 return;
640             }
641 
642             mReportUnhandledFillRequest = false;
643             throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
644                     + mFillRequests);
645         }
646 
647         /**
648          * Gets the current number of unhandled requests.
649          */
getNumberUnhandledFillRequests()650         public int getNumberUnhandledFillRequests() {
651             return mFillRequests.size();
652         }
653 
654         /**
655          * Gets the next save request, in the order received.
656          *
657          * <p>Typically called at the end of a test case, to assert the initial request.
658          */
getNextSaveRequest()659         public SaveRequest getNextSaveRequest() {
660             SaveRequest request;
661             try {
662                 request = mSaveRequests.poll(SAVE_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
663             } catch (InterruptedException e) {
664                 Thread.currentThread().interrupt();
665                 throw new IllegalStateException("Interrupted", e);
666             }
667             if (request == null) {
668                 throw new RetryableException(SAVE_TIMEOUT, "onSaveRequest() not called");
669             }
670             return request;
671         }
672 
673         /**
674          * Asserts all
675          * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)
676          * save requests} received by the service were properly
677          * {@link #getNextFillRequest() handled} by the test case.
678          */
assertNoUnhandledSaveRequests()679         public void assertNoUnhandledSaveRequests() {
680             if (mSaveRequests.isEmpty()) return; // Good job, test case!
681 
682             if (!mReportUnhandledSaveRequest) {
683                 // Just log, so it's not thrown again on @After if already thrown on main body
684                 Log.d(TAG, "assertNoUnhandledSaveRequests(): already reported, "
685                         + "but logging just in case: " + mSaveRequests);
686                 return;
687             }
688 
689             mReportUnhandledSaveRequest = false;
690             throw new AssertionError(mSaveRequests.size() + " unhandled save requests: "
691                     + mSaveRequests);
692         }
693 
setHandler(Handler handler)694         public void setHandler(Handler handler) {
695             mHandler = handler;
696         }
697 
698         /**
699          * Resets its internal state.
700          */
reset()701         public void reset() {
702             mLazyResponses.clear();
703             mResponses.clear();
704             mFillRequests.clear();
705             mSaveRequests.clear();
706             mExceptions = null;
707             mOnSaveIntentSender = null;
708             mAcceptedPackageName = null;
709             mReportUnhandledFillRequest = true;
710             mReportUnhandledSaveRequest = true;
711             mFillEventHistory = null;
712             mSessionDestroyedCount = 0;
713         }
714 
onFillRequest(List<FillContext> contexts, List<String> hints, Bundle data, CancellationSignal cancellationSignal, FillCallback callback, int flags, InlineSuggestionsRequest inlineRequest, IntentSender delayFillIntentSender, int requestId)715         private void onFillRequest(List<FillContext> contexts, List<String> hints, Bundle data,
716                 CancellationSignal cancellationSignal, FillCallback callback, int flags,
717                 InlineSuggestionsRequest inlineRequest, IntentSender delayFillIntentSender,
718                 int requestId) {
719             boolean hasLazyResponse = mLazyResponses.size() > 0;
720             try {
721                 CannedFillResponse response = null;
722                 try {
723                     if (hasLazyResponse) {
724                         response =
725                                 mLazyResponses
726                                         .poll()
727                                         .apply(
728                                                 new FillRequest(
729                                                         contexts,
730                                                         hints,
731                                                         data,
732                                                         cancellationSignal,
733                                                         callback,
734                                                         flags,
735                                                         inlineRequest,
736                                                         delayFillIntentSender,
737                                                         requestId));
738                     } else {
739                         response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
740                     }
741                 } catch (InterruptedException e) {
742                     Log.w(TAG, "Interrupted getting CannedResponse: " + e);
743                     Thread.currentThread().interrupt();
744                     addException(e);
745                     return;
746                 }
747                 if (response == null) {
748                     final String activityName = getActivityName(contexts);
749                     final String msg = "onFillRequest() for activity " + activityName
750                             + " received when no canned response was set.";
751                     dumpStructure(msg, contexts);
752                     return;
753                 }
754                 if (response.getResponseType() == NULL) {
755                     Log.d(TAG, "onFillRequest(): replying with null");
756                     callback.onSuccess(null);
757                     return;
758                 }
759 
760                 if (response.getResponseType() == TIMEOUT) {
761                     Log.d(TAG, "onFillRequest(): not replying at all");
762                     return;
763                 }
764 
765                 if (response.getResponseType() == FAILURE) {
766                     Log.d(TAG, "onFillRequest(): replying with failure");
767                     callback.onFailure("D'OH!");
768                     return;
769                 }
770 
771                 if (response.getResponseType() == ResponseType.NO_MORE) {
772                     Log.w(TAG, "onFillRequest(): replying with null when not expecting more");
773                     addException(new IllegalStateException("got unexpected request"));
774                     callback.onSuccess(null);
775                     return;
776                 }
777 
778                 final String failureMessage = response.getFailureMessage();
779                 if (failureMessage != null) {
780                     Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage);
781                     callback.onFailure(failureMessage);
782                     return;
783                 }
784 
785                 final FillResponse fillResponse;
786 
787                 switch (mIdMode) {
788                     case RESOURCE_ID:
789                         fillResponse = response.asFillResponse(contexts,
790                                 (id) -> Helper.findNodeByResourceId(contexts, id));
791                         break;
792                     case HTML_NAME:
793                         fillResponse = response.asFillResponse(contexts,
794                                 (name) -> Helper.findNodeByHtmlName(contexts, name));
795                         break;
796                     case HTML_NAME_OR_RESOURCE_ID:
797                         fillResponse = response.asFillResponse(contexts,
798                                 (id) -> Helper.findNodeByHtmlNameOrResourceId(contexts, id));
799                         break;
800                     case PCC_ID:
801                         // TODO: SaveInfo undetermined for PCC
802                         fillResponse = response.asPccFillResponse(contexts,
803                                 (id) -> Helper.findNodeByResourceId(contexts, id));
804                         break;
805                     default:
806                         throw new IllegalStateException("Unknown id mode: " + mIdMode);
807                 }
808 
809                 if (response.getResponseType() == ResponseType.DELAY) {
810                     mHandler.postDelayed(() -> {
811                         Log.v(TAG,
812                                 "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
813                         callback.onSuccess(fillResponse);
814                         // Add a fill request to let test case know response was sent.
815                         Helper.offer(mFillRequests,
816                                 new FillRequest(contexts, hints, data, cancellationSignal, callback,
817                                         flags, inlineRequest, delayFillIntentSender, requestId),
818                                 CONNECTION_TIMEOUT.ms());
819                     }, RESPONSE_DELAY_MS);
820                 } else {
821                     Log.v(TAG, "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
822                     callback.onSuccess(fillResponse);
823                 }
824             } catch (Throwable t) {
825                 addException(t);
826             } finally {
827                 if (!hasLazyResponse) {
828                     Helper.offer(mFillRequests, new FillRequest(contexts, hints, data,
829                             cancellationSignal, callback, flags, inlineRequest,
830                             delayFillIntentSender, requestId),
831                             CONNECTION_TIMEOUT.ms());
832                 }
833             }
834         }
835 
onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback, List<String> datasetIds)836         private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
837                 List<String> datasetIds) {
838             Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender);
839 
840             try {
841                 if (mOnSaveIntentSender != null) {
842                     callback.onSuccess(mOnSaveIntentSender);
843                 } else {
844                     callback.onSuccess();
845                 }
846             } finally {
847                 Helper.offer(mSaveRequests, new SaveRequest(contexts, data, callback, datasetIds),
848                         CONNECTION_TIMEOUT.ms());
849             }
850         }
851 
dump(PrintWriter pw)852         private void dump(PrintWriter pw) {
853             pw.print("mResponses: "); pw.println(mResponses);
854             pw.print("mFillRequests: "); pw.println(mFillRequests);
855             pw.print("mSaveRequests: "); pw.println(mSaveRequests);
856             pw.print("mExceptions: "); pw.println(mExceptions);
857             pw.print("mOnSaveIntentSender: "); pw.println(mOnSaveIntentSender);
858             pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
859             pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
860             pw.print("mReportUnhandledFillRequest: "); pw.println(mReportUnhandledSaveRequest);
861             pw.print("mIdMode: "); pw.println(mIdMode);
862             pw.print("mFillEventHistory: "); pw.println(mFillEventHistory);
863         }
864     }
865 }