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