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