• 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.getHints(),
245                         request.getClientState(),
246                         cancellationSignal, callback, request.getFlags(),
247                         request.getInlineSuggestionsRequest(),
248                         request.getDelayedFillIntentSender(),
249                         request.getId()));
250     }
251 
252     @Override
onSaveRequest(android.service.autofill.SaveRequest request, SaveCallback callback)253     public void onSaveRequest(android.service.autofill.SaveRequest request,
254             SaveCallback callback) {
255         if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
256             dumpSelf();
257             sReplier.addException(
258                     new IllegalStateException("onSaveRequest() called when disconnected"));
259         }
260         mHandler.post(()->handleSaveRequest(request, callback));
261     }
262 
handleSaveRequest(android.service.autofill.SaveRequest request, SaveCallback callback)263     private void handleSaveRequest(android.service.autofill.SaveRequest request,
264             SaveCallback callback) {
265         final ComponentName component = getLastActivityComponent(request.getFillContexts());
266         if (!TestNameUtils.isRunningTest()) {
267             Log.e(TAG, "onSaveRequest(" + component + ") called after tests finished");
268             return;
269         }
270         if (!fromSamePackage(component)) {
271             Log.w(TAG, "Ignoring onSaveRequest() from different package: " + component);
272             return;
273         }
274         if (DUMP_SAVE_REQUESTS) {
275             dumpStructure("onSaveRequest()", request.getFillContexts());
276         } else {
277             Log.i(TAG, "onSaveRequest() for " + component.toShortString());
278         }
279         mHandler.post(() -> sReplier.onSaveRequest(request.getFillContexts(),
280                 request.getClientState(), callback,
281                 request.getDatasetIds()));
282     }
283 
284     @Override
onSavedDatasetsInfoRequest(@onNull SavedDatasetsInfoCallback callback)285     public void onSavedDatasetsInfoRequest(@NonNull SavedDatasetsInfoCallback callback) {
286         if (sSavedDatasetsInfoReplier == null) {
287             super.onSavedDatasetsInfoRequest(callback);
288         } else {
289             sSavedDatasetsInfoReplier.accept(callback);
290         }
291     }
292 
setSavedDatasetsInfoReplier( @ullable Consumer<SavedDatasetsInfoCallback> savedDatasetsInfoReplier)293     public static void setSavedDatasetsInfoReplier(
294             @Nullable Consumer<SavedDatasetsInfoCallback> savedDatasetsInfoReplier) {
295         sSavedDatasetsInfoReplier = savedDatasetsInfoReplier;
296     }
297 
isConnected()298     public static boolean isConnected() {
299         return sConnected.get();
300     }
301 
fromSamePackage(ComponentName component)302     private boolean fromSamePackage(ComponentName component) {
303         final String actualPackage = component.getPackageName();
304         if (!actualPackage.equals(getPackageName())
305                 && !actualPackage.equals(sReplier.mAcceptedPackageName)) {
306             Log.w(TAG, "Got request from package " + actualPackage);
307             return false;
308         }
309         return true;
310     }
311 
getLastActivityComponent(List<FillContext> contexts)312     private ComponentName getLastActivityComponent(List<FillContext> contexts) {
313         return contexts.get(contexts.size() - 1).getStructure().getActivityComponent();
314     }
315 
dumpSelf()316     private void dumpSelf()  {
317         try {
318             try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
319                 dump(null, pw, null);
320                 pw.flush();
321                 final String dump = sw.toString();
322                 Log.e(TAG, "dumpSelf(): " + dump);
323             }
324         } catch (IOException e) {
325             Log.e(TAG, "I don't always fail to dump, but when I do, I dump the failure", e);
326         }
327     }
328 
329     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)330     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
331         pw.print("sConnected: "); pw.println(sConnected);
332         pw.print("mConnected: "); pw.println(mConnected);
333         pw.print("sInstance: "); pw.println(sInstance);
334         pw.println("sReplier: "); sReplier.dump(pw);
335     }
336 
337     /**
338      * Waits until {@link #onConnected()} is called, or fails if it times out.
339      *
340      * <p>This method is useful on tests that explicitly verifies the connection, but should be
341      * avoided in other tests, as it adds extra time to the test execution (and flakiness in cases
342      * where the service might have being disconnected already; for example, if the fill request
343      * was replied with a {@code null} response) - if a text needs to block until the service
344      * receives a callback, it should use {@link Replier#getNextFillRequest()} instead.
345      */
waitUntilConnected()346     public static void waitUntilConnected() throws Exception {
347         waitConnectionState(CONNECTION_TIMEOUT, true);
348     }
349 
350     /**
351      * Waits until {@link #onDisconnected()} is called, or fails if it times out.
352      *
353      * <p>This method is useful on tests that explicitly verifies the connection, but should be
354      * avoided in other tests, as it adds extra time to the test execution.
355      */
waitUntilDisconnected()356     public static void waitUntilDisconnected() throws Exception {
357         waitConnectionState(IDLE_UNBIND_TIMEOUT, false);
358     }
359 
waitConnectionState(Timeout timeout, boolean expected)360     private static void waitConnectionState(Timeout timeout, boolean expected) throws Exception {
361         timeout.run("wait for connected=" + expected,  () -> {
362             return isConnected() == expected ? Boolean.TRUE : null;
363         });
364     }
365 
366     /**
367      * Gets the {@link Replier} singleton.
368      */
getReplier()369     public static Replier getReplier() {
370         return sReplier;
371     }
372 
resetStaticState()373     public static void resetStaticState() {
374         sInstance.set(null);
375         sConnected.set(false);
376         sServiceLabel = SERVICE_CLASS;
377         sSavedDatasetsInfoReplier = null;
378     }
379 
380     /**
381      * POJO representation of the contents of a
382      * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
383      * CancellationSignal, FillCallback)} that can be asserted at the end of a test case.
384      */
385     public static final class FillRequest {
386         public final AssistStructure structure;
387         public final List<FillContext> contexts;
388         public final Bundle data;
389         public final CancellationSignal cancellationSignal;
390         public final FillCallback callback;
391         public final int flags;
392         public final InlineSuggestionsRequest inlineRequest;
393         public final IntentSender delayFillIntentSender;
394         public final int requestId;
395         public final List<String> hints;
396 
FillRequest(List<FillContext> contexts, List<String> hints, Bundle data, CancellationSignal cancellationSignal, FillCallback callback, int flags, InlineSuggestionsRequest inlineRequest, IntentSender delayFillIntentSender, int requestId)397         private FillRequest(List<FillContext> contexts, List<String> hints, Bundle data,
398                 CancellationSignal cancellationSignal, FillCallback callback, int flags,
399                 InlineSuggestionsRequest inlineRequest,
400                 IntentSender delayFillIntentSender,
401                 int requestId) {
402             this.contexts = contexts;
403             this.hints = hints;
404             this.data = data;
405             this.cancellationSignal = cancellationSignal;
406             this.callback = callback;
407             this.flags = flags;
408             this.structure = contexts.get(contexts.size() - 1).getStructure();
409             this.inlineRequest = inlineRequest;
410             this.delayFillIntentSender = delayFillIntentSender;
411             this.requestId = requestId;
412         }
413 
414         @Override
toString()415         public String toString() {
416             return "FillRequest[activity=" + getActivityName(contexts) + ", flags=" + flags
417                     + ", bundle=" + data + ", structure=" + Helper.toString(structure) + "]";
418         }
419     }
420 
421     /**
422      * POJO representation of the contents of a
423      * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)}
424      * that can be asserted at the end of a test case.
425      */
426     public static final class SaveRequest {
427         public final List<FillContext> contexts;
428         public final AssistStructure structure;
429         public final Bundle data;
430         public final SaveCallback callback;
431         public final List<String> datasetIds;
432 
SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback, List<String> datasetIds)433         private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
434                 List<String> datasetIds) {
435             if (contexts != null && contexts.size() > 0) {
436                 structure = contexts.get(contexts.size() - 1).getStructure();
437             } else {
438                 structure = null;
439             }
440             this.contexts = contexts;
441             this.data = data;
442             this.callback = callback;
443             this.datasetIds = datasetIds;
444         }
445 
446         @Override
toString()447         public String toString() {
448             return "SaveRequest:" + getActivityName(contexts);
449         }
450     }
451 
452     /**
453      * Object used to answer a
454      * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
455      * CancellationSignal, FillCallback)}
456      * on behalf of a unit test method.
457      */
458     public static final class Replier {
459 
460         private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>();
461         private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>();
462         private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>();
463 
464         private List<Throwable> mExceptions;
465         private IntentSender mOnSaveIntentSender;
466         private String mAcceptedPackageName;
467 
468         private Handler mHandler;
469 
470         private boolean mReportUnhandledFillRequest = true;
471         private boolean mReportUnhandledSaveRequest = true;
472 
Replier()473         private Replier() {
474         }
475 
476         private IdMode mIdMode = IdMode.RESOURCE_ID;
477 
setIdMode(IdMode mode)478         public void setIdMode(IdMode mode) {
479             this.mIdMode = mode;
480         }
481 
acceptRequestsFromPackage(String packageName)482         public void acceptRequestsFromPackage(String packageName) {
483             mAcceptedPackageName = packageName;
484         }
485 
486         /**
487          * Gets the exceptions thrown asynchronously, if any.
488          */
489         @Nullable
getExceptions()490         public List<Throwable> getExceptions() {
491             return mExceptions;
492         }
493 
addException(@ullable Throwable e)494         private void addException(@Nullable Throwable e) {
495             if (e == null) return;
496 
497             if (mExceptions == null) {
498                 mExceptions = new ArrayList<>();
499             }
500             mExceptions.add(e);
501         }
502 
503         /**
504          * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just
505          * one {@link Dataset}.
506          */
addResponse(CannedDataset dataset)507         public Replier addResponse(CannedDataset dataset) {
508             return addResponse(new CannedFillResponse.Builder()
509                     .addDataset(dataset)
510                     .build());
511         }
512 
513         /**
514          * Sets the expectation for the next {@code onFillRequest}.
515          */
addResponse(CannedFillResponse response)516         public Replier addResponse(CannedFillResponse response) {
517             if (response == null) {
518                 throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
519             }
520             mResponses.add(response);
521             return this;
522         }
523 
524         /**
525          * Sets the {@link IntentSender} that is passed to
526          * {@link SaveCallback#onSuccess(IntentSender)}.
527          */
setOnSave(IntentSender intentSender)528         public Replier setOnSave(IntentSender intentSender) {
529             mOnSaveIntentSender = intentSender;
530             return this;
531         }
532 
533         /**
534          * Gets the next fill request, in the order received.
535          */
getNextFillRequest()536         public FillRequest getNextFillRequest() {
537             FillRequest request;
538             try {
539                 request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
540             } catch (InterruptedException e) {
541                 Thread.currentThread().interrupt();
542                 throw new IllegalStateException("Interrupted", e);
543             }
544             if (request == null) {
545                 throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called");
546             }
547             return request;
548         }
549 
550         /**
551          * Asserts that {@link #onFillRequest(List, Bundle, CancellationSignal, FillCallback, int)}
552          * was not called.
553          *
554          * <p>Should only be called in cases where it's not expected to be called, as it will
555          * sleep for a few ms.
556          */
assertOnFillRequestNotCalled()557         public void assertOnFillRequestNotCalled() {
558             SystemClock.sleep(FILL_TIMEOUT.getMaxValue());
559             assertThat(mFillRequests).isEmpty();
560         }
561 
562         /**
563          * Asserts all {@link AutofillService#onFillRequest(
564          * android.service.autofill.FillRequest,  CancellationSignal, FillCallback) fill requests}
565          * received by the service were properly {@link #getNextFillRequest() handled} by the test
566          * case.
567          */
assertNoUnhandledFillRequests()568         public void assertNoUnhandledFillRequests() {
569             if (mFillRequests.isEmpty()) return; // Good job, test case!
570 
571             if (!mReportUnhandledFillRequest) {
572                 // Just log, so it's not thrown again on @After if already thrown on main body
573                 Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
574                         + "but logging just in case: " + mFillRequests);
575                 return;
576             }
577 
578             mReportUnhandledFillRequest = false;
579             throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
580                     + mFillRequests);
581         }
582 
583         /**
584          * Gets the current number of unhandled requests.
585          */
getNumberUnhandledFillRequests()586         public int getNumberUnhandledFillRequests() {
587             return mFillRequests.size();
588         }
589 
590         /**
591          * Gets the next save request, in the order received.
592          *
593          * <p>Typically called at the end of a test case, to assert the initial request.
594          */
getNextSaveRequest()595         public SaveRequest getNextSaveRequest() {
596             SaveRequest request;
597             try {
598                 request = mSaveRequests.poll(SAVE_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
599             } catch (InterruptedException e) {
600                 Thread.currentThread().interrupt();
601                 throw new IllegalStateException("Interrupted", e);
602             }
603             if (request == null) {
604                 throw new RetryableException(SAVE_TIMEOUT, "onSaveRequest() not called");
605             }
606             return request;
607         }
608 
609         /**
610          * Asserts all
611          * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)
612          * save requests} received by the service were properly
613          * {@link #getNextFillRequest() handled} by the test case.
614          */
assertNoUnhandledSaveRequests()615         public void assertNoUnhandledSaveRequests() {
616             if (mSaveRequests.isEmpty()) return; // Good job, test case!
617 
618             if (!mReportUnhandledSaveRequest) {
619                 // Just log, so it's not thrown again on @After if already thrown on main body
620                 Log.d(TAG, "assertNoUnhandledSaveRequests(): already reported, "
621                         + "but logging just in case: " + mSaveRequests);
622                 return;
623             }
624 
625             mReportUnhandledSaveRequest = false;
626             throw new AssertionError(mSaveRequests.size() + " unhandled save requests: "
627                     + mSaveRequests);
628         }
629 
setHandler(Handler handler)630         public void setHandler(Handler handler) {
631             mHandler = handler;
632         }
633 
634         /**
635          * Resets its internal state.
636          */
reset()637         public void reset() {
638             mResponses.clear();
639             mFillRequests.clear();
640             mSaveRequests.clear();
641             mExceptions = null;
642             mOnSaveIntentSender = null;
643             mAcceptedPackageName = null;
644             mReportUnhandledFillRequest = true;
645             mReportUnhandledSaveRequest = true;
646         }
647 
onFillRequest(List<FillContext> contexts, List<String> hints, Bundle data, CancellationSignal cancellationSignal, FillCallback callback, int flags, InlineSuggestionsRequest inlineRequest, IntentSender delayFillIntentSender, int requestId)648         private void onFillRequest(List<FillContext> contexts, List<String> hints, Bundle data,
649                 CancellationSignal cancellationSignal, FillCallback callback, int flags,
650                 InlineSuggestionsRequest inlineRequest, IntentSender delayFillIntentSender,
651                 int requestId) {
652             try {
653                 CannedFillResponse response = null;
654                 try {
655                     response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
656                 } catch (InterruptedException e) {
657                     Log.w(TAG, "Interrupted getting CannedResponse: " + e);
658                     Thread.currentThread().interrupt();
659                     addException(e);
660                     return;
661                 }
662                 if (response == null) {
663                     final String activityName = getActivityName(contexts);
664                     final String msg = "onFillRequest() for activity " + activityName
665                             + " received when no canned response was set.";
666                     dumpStructure(msg, contexts);
667                     return;
668                 }
669                 if (response.getResponseType() == NULL) {
670                     Log.d(TAG, "onFillRequest(): replying with null");
671                     callback.onSuccess(null);
672                     return;
673                 }
674 
675                 if (response.getResponseType() == TIMEOUT) {
676                     Log.d(TAG, "onFillRequest(): not replying at all");
677                     return;
678                 }
679 
680                 if (response.getResponseType() == FAILURE) {
681                     Log.d(TAG, "onFillRequest(): replying with failure");
682                     callback.onFailure("D'OH!");
683                     return;
684                 }
685 
686                 if (response.getResponseType() == ResponseType.NO_MORE) {
687                     Log.w(TAG, "onFillRequest(): replying with null when not expecting more");
688                     addException(new IllegalStateException("got unexpected request"));
689                     callback.onSuccess(null);
690                     return;
691                 }
692 
693                 final String failureMessage = response.getFailureMessage();
694                 if (failureMessage != null) {
695                     Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage);
696                     callback.onFailure(failureMessage);
697                     return;
698                 }
699 
700                 final FillResponse fillResponse;
701 
702                 switch (mIdMode) {
703                     case RESOURCE_ID:
704                         fillResponse = response.asFillResponse(contexts,
705                                 (id) -> Helper.findNodeByResourceId(contexts, id));
706                         break;
707                     case HTML_NAME:
708                         fillResponse = response.asFillResponse(contexts,
709                                 (name) -> Helper.findNodeByHtmlName(contexts, name));
710                         break;
711                     case HTML_NAME_OR_RESOURCE_ID:
712                         fillResponse = response.asFillResponse(contexts,
713                                 (id) -> Helper.findNodeByHtmlNameOrResourceId(contexts, id));
714                         break;
715                     case PCC_ID:
716                         // TODO: SaveInfo undetermined for PCC
717                         fillResponse = response.asPccFillResponse(contexts,
718                                 (id) -> Helper.findNodeByResourceId(contexts, id));
719                         break;
720                     default:
721                         throw new IllegalStateException("Unknown id mode: " + mIdMode);
722                 }
723 
724                 if (response.getResponseType() == ResponseType.DELAY) {
725                     mHandler.postDelayed(() -> {
726                         Log.v(TAG,
727                                 "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
728                         callback.onSuccess(fillResponse);
729                         // Add a fill request to let test case know response was sent.
730                         Helper.offer(mFillRequests,
731                                 new FillRequest(contexts, hints, data, cancellationSignal, callback,
732                                         flags, inlineRequest, delayFillIntentSender, requestId),
733                                 CONNECTION_TIMEOUT.ms());
734                     }, RESPONSE_DELAY_MS);
735                 } else {
736                     Log.v(TAG, "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
737                     callback.onSuccess(fillResponse);
738                 }
739             } catch (Throwable t) {
740                 addException(t);
741             } finally {
742                 Helper.offer(mFillRequests, new FillRequest(contexts, hints, data,
743                         cancellationSignal, callback, flags, inlineRequest,
744                         delayFillIntentSender, requestId),
745                         CONNECTION_TIMEOUT.ms());
746             }
747         }
748 
onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback, List<String> datasetIds)749         private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
750                 List<String> datasetIds) {
751             Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender);
752 
753             try {
754                 if (mOnSaveIntentSender != null) {
755                     callback.onSuccess(mOnSaveIntentSender);
756                 } else {
757                     callback.onSuccess();
758                 }
759             } finally {
760                 Helper.offer(mSaveRequests, new SaveRequest(contexts, data, callback, datasetIds),
761                         CONNECTION_TIMEOUT.ms());
762             }
763         }
764 
dump(PrintWriter pw)765         private void dump(PrintWriter pw) {
766             pw.print("mResponses: "); pw.println(mResponses);
767             pw.print("mFillRequests: "); pw.println(mFillRequests);
768             pw.print("mSaveRequests: "); pw.println(mSaveRequests);
769             pw.print("mExceptions: "); pw.println(mExceptions);
770             pw.print("mOnSaveIntentSender: "); pw.println(mOnSaveIntentSender);
771             pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
772             pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
773             pw.print("mReportUnhandledFillRequest: "); pw.println(mReportUnhandledSaveRequest);
774             pw.print("mIdMode: "); pw.println(mIdMode);
775         }
776     }
777 }