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