1 /*
2  * Copyright (C) 2019 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.AugmentedHelper.await;
20 import static android.autofillservice.cts.testcore.AugmentedHelper.getActivityName;
21 import static android.autofillservice.cts.testcore.AugmentedTimeouts.AUGMENTED_CONNECTION_TIMEOUT;
22 import static android.autofillservice.cts.testcore.AugmentedTimeouts.AUGMENTED_FILL_TIMEOUT;
23 import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.AugmentedResponseType.NULL;
24 import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.AugmentedResponseType.TIMEOUT;
25 import static android.autofillservice.cts.testcore.Timeouts.FILL_EVENTS_TIMEOUT;
26 
27 import static com.google.common.truth.Truth.assertWithMessage;
28 
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.os.CancellationSignal;
32 import android.os.Handler;
33 import android.os.HandlerThread;
34 import android.os.SystemClock;
35 import android.service.autofill.FillEventHistory;
36 import android.service.autofill.FillEventHistory.Event;
37 import android.service.autofill.augmented.AugmentedAutofillService;
38 import android.service.autofill.augmented.FillCallback;
39 import android.service.autofill.augmented.FillController;
40 import android.service.autofill.augmented.FillRequest;
41 import android.service.autofill.augmented.FillResponse;
42 import android.util.ArraySet;
43 import android.util.Log;
44 import android.view.autofill.AutofillManager;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 
49 import com.android.compatibility.common.util.RetryableException;
50 import com.android.compatibility.common.util.TestNameUtils;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.concurrent.BlockingQueue;
55 import java.util.concurrent.CountDownLatch;
56 import java.util.concurrent.LinkedBlockingQueue;
57 import java.util.concurrent.TimeUnit;
58 
59 /**
60  * Implementation of {@link AugmentedAutofillService} used in the tests.
61  */
62 public class CtsAugmentedAutofillService extends AugmentedAutofillService {
63 
64     private static final String TAG = CtsAugmentedAutofillService.class.getSimpleName();
65 
66     public static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
67     public static final String SERVICE_CLASS = CtsAugmentedAutofillService.class.getSimpleName();
68 
69     public static final String SERVICE_NAME = SERVICE_PACKAGE + "/.testcore." + SERVICE_CLASS;
70 
71     private static final AugmentedReplier sAugmentedReplier = new AugmentedReplier();
72 
73     // We must handle all requests in a separate thread as the service's main thread is the also
74     // the UI thread of the test process and we don't want to hose it in case of failures here
75     private static final HandlerThread sMyThread = new HandlerThread("MyAugmentedServiceThread");
76     private final Handler mHandler;
77 
78     private static ServiceWatcher sServiceWatcher;
79 
80     static {
Log.i(TAG, "Starting thread " + sMyThread)81         Log.i(TAG, "Starting thread " + sMyThread);
sMyThread.start()82         sMyThread.start();
83     }
84 
CtsAugmentedAutofillService()85     public CtsAugmentedAutofillService() {
86         mHandler = Handler.createAsync(sMyThread.getLooper());
87     }
88 
89     @NonNull
setServiceWatcher()90     public static ServiceWatcher setServiceWatcher() {
91         if (sServiceWatcher != null) {
92             throw new IllegalStateException("There Can Be Only One during testing!");
93         }
94         sServiceWatcher = new ServiceWatcher();
95         return sServiceWatcher;
96     }
97 
98 
resetStaticState()99     public static void resetStaticState() {
100         // Found the sServiceWatcher is null when AugmentedAutofillService is connected. It should
101         // be set when enabling augmented autofill service. But when AugmentedAutofillService is
102         // connected, the sServiceWatcher is null. No onDisconnected log ir printed, the
103         // sServiceWatcher may be reset here. Add log to help future flaky debugging.
104         Log.d(TAG, "resetStaticState()", new Throwable());
105         List<Throwable> exceptions = sAugmentedReplier.mExceptions;
106         if (exceptions != null) {
107             exceptions.clear();
108         }
109         // TODO(b/123540602): should probably set sInstance to null as well, but first we would need
110         // to make sure each test unbinds the service.
111 
112         // TODO(b/123540602): each test should use a different service instance, but we need
113         // to provide onConnected() / onDisconnected() methods first and then change the infra so
114         // we can wait for those
115 
116         if (sServiceWatcher != null) {
117             Log.wtf(TAG, "resetStaticState(): should not have sServiceWatcher");
118             sServiceWatcher = null;
119         }
120     }
121 
122     @Override
onConnected()123     public void onConnected() {
124         Log.i(TAG, "onConnected(): sServiceWatcher=" + sServiceWatcher);
125 
126         if (sServiceWatcher == null) {
127             // (b/230554011):onConnected()/onConnected() may be called multiple times during testing
128             // or during the test setup/destroyed. Sometimes the test actually passes but this
129             // symtom causes the test addException(), the test result fails finally. Instead of
130             // addException only logging here
131             Log.w(TAG, "onConnected() without a watcher");
132             return;
133         }
134 
135         final AutofillManager afm = getApplication().getSystemService(AutofillManager.class);
136         if (afm == null) {
137             Log.e(TAG, "No AutofillManager on application context on onConnected()");
138             addException("No AutofillManager on application context on onConnected()");
139             return;
140         }
141         Log.d(TAG, "Set allowlist with " + Helper.MY_PACKAGE + " for augmented autofill");
142         final ArraySet<String> packages = new ArraySet<>(1);
143         packages.add(Helper.MY_PACKAGE);
144         afm.setAugmentedAutofillWhitelist(packages, /* activities= */ null);
145 
146         sServiceWatcher.mService = this;
147         sServiceWatcher.mCreated.countDown();
148     }
149 
150     @Override
onDisconnected()151     public void onDisconnected() {
152         Log.i(TAG, "onDisconnected(): sServiceWatcher=" + sServiceWatcher);
153 
154         if (sServiceWatcher == null) {
155             Log.w(TAG, "onDisconnected() without a watcher");
156             return;
157         }
158 
159         sServiceWatcher.mDestroyed.countDown();
160         sServiceWatcher.mService = null;
161         sServiceWatcher = null;
162     }
163 
getFillEventHistory(int expectedSize)164     public FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
165         return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
166             final FillEventHistory history = getFillEventHistory();
167             if (history == null) {
168                 return null;
169             }
170             final List<Event> events = history.getEvents();
171             if (events != null) {
172                 assertWithMessage("Didn't get " + expectedSize + " events yet: " + events).that(
173                         events.size()).isEqualTo(expectedSize);
174             } else {
175                 assertWithMessage("Events is null (expecting " + expectedSize + ")").that(
176                         expectedSize).isEqualTo(0);
177                 return null;
178             }
179             return history;
180         });
181     }
182 
183     @Override
onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillController controller, FillCallback callback)184     public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
185             FillController controller, FillCallback callback) {
186         Log.i(TAG, "onFillRequest(): " + AugmentedHelper.toString(request));
187 
188         final ComponentName component = request.getActivityComponent();
189 
190         if (!TestNameUtils.isRunningTest()) {
191             Log.e(TAG, "onFillRequest(" + component + ") called after tests finished");
192             return;
193         }
194         mHandler.post(() -> sAugmentedReplier.handleOnFillRequest(getApplicationContext(), request,
195                 cancellationSignal, controller, callback));
196     }
197 
198     /**
199      * Gets the {@link AugmentedReplier} singleton.
200      */
getAugmentedReplier()201     public static AugmentedReplier getAugmentedReplier() {
202         return sAugmentedReplier;
203     }
204 
addException(@onNull String fmt, @Nullable Object...args)205     private static void addException(@NonNull String fmt, @Nullable Object...args) {
206         final String msg = String.format(fmt, args);
207         Log.e(TAG, msg);
208         sAugmentedReplier.addException(new IllegalStateException(msg));
209     }
210 
211     /**
212      * POJO representation of the contents of a {@link FillRequest}
213      * that can be asserted at the end of a test case.
214      */
215     public static final class AugmentedFillRequest {
216         public final FillRequest request;
217         public final CancellationSignal cancellationSignal;
218         public final FillController controller;
219         public final FillCallback callback;
220 
AugmentedFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillController controller, FillCallback callback)221         private AugmentedFillRequest(FillRequest request, CancellationSignal cancellationSignal,
222                 FillController controller, FillCallback callback) {
223             this.request = request;
224             this.cancellationSignal = cancellationSignal;
225             this.controller = controller;
226             this.callback = callback;
227         }
228 
229         @Override
toString()230         public String toString() {
231             return "AugmentedFillRequest[activity=" + getActivityName(request) + ", request="
232                     + AugmentedHelper.toString(request) + "]";
233         }
234     }
235 
236     /**
237      * Object used to answer a
238      * {@link AugmentedAutofillService#onFillRequest(FillRequest, CancellationSignal,
239      * FillController, FillCallback)} on behalf of a unit test method.
240      */
241     public static final class AugmentedReplier {
242 
243         private final BlockingQueue<CannedAugmentedFillResponse> mResponses =
244                 new LinkedBlockingQueue<>();
245         private final BlockingQueue<AugmentedFillRequest> mFillRequests =
246                 new LinkedBlockingQueue<>();
247 
248         private List<Throwable> mExceptions;
249         private boolean mReportUnhandledFillRequest = true;
250 
AugmentedReplier()251         private AugmentedReplier() {
252         }
253 
254         /**
255          * Gets the exceptions thrown asynchronously, if any.
256          */
257         @Nullable
getExceptions()258         public List<Throwable> getExceptions() {
259             return mExceptions;
260         }
261 
addException(@ullable Throwable e)262         private void addException(@Nullable Throwable e) {
263             if (e == null) return;
264 
265             if (mExceptions == null) {
266                 mExceptions = new ArrayList<>();
267             }
268             mExceptions.add(e);
269         }
270 
271         /**
272          * Sets the expectation for the next {@code onFillRequest}.
273          */
addResponse(@onNull CannedAugmentedFillResponse response)274         public AugmentedReplier addResponse(@NonNull CannedAugmentedFillResponse response) {
275             if (response == null) {
276                 throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
277             }
278             mResponses.add(response);
279             return this;
280         }
281         /**
282          * Gets the next fill request, in the order received.
283          */
getNextFillRequest()284         public AugmentedFillRequest getNextFillRequest() {
285             AugmentedFillRequest request;
286             try {
287                 request = mFillRequests.poll(AUGMENTED_FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
288             } catch (InterruptedException e) {
289                 Thread.currentThread().interrupt();
290                 throw new IllegalStateException("Interrupted", e);
291             }
292             if (request == null) {
293                 throw new RetryableException(AUGMENTED_FILL_TIMEOUT, "onFillRequest() not called");
294             }
295             return request;
296         }
297 
298         /**
299          * Asserts all {@link AugmentedAutofillService#onFillRequest(FillRequest,
300          * CancellationSignal, FillController, FillCallback)} received by the service were properly
301          * {@link #getNextFillRequest() handled} by the test case.
302          */
assertNoUnhandledFillRequests()303         public void assertNoUnhandledFillRequests() {
304             if (mFillRequests.isEmpty()) return; // Good job, test case!
305 
306             if (!mReportUnhandledFillRequest) {
307                 // Just log, so it's not thrown again on @After if already thrown on main body
308                 Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
309                         + "but logging just in case: " + mFillRequests);
310                 return;
311             }
312 
313             mReportUnhandledFillRequest = false;
314             throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
315                     + mFillRequests);
316         }
317 
318         /**
319          * Gets the current number of unhandled requests.
320          */
getNumberUnhandledFillRequests()321         public int getNumberUnhandledFillRequests() {
322             return mFillRequests.size();
323         }
324 
325         /**
326          * Resets its internal state.
327          */
reset()328         public void reset() {
329             mResponses.clear();
330             mFillRequests.clear();
331             mExceptions = null;
332             mReportUnhandledFillRequest = true;
333         }
334 
handleOnFillRequest(@onNull Context context, @NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller, @NonNull FillCallback callback)335         private void handleOnFillRequest(@NonNull Context context, @NonNull FillRequest request,
336                 @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller,
337                 @NonNull FillCallback callback) {
338             final AugmentedFillRequest myRequest = new AugmentedFillRequest(request,
339                     cancellationSignal, controller, callback);
340             Log.d(TAG, "offering " + myRequest);
341             Helper.offer(mFillRequests, myRequest, AUGMENTED_CONNECTION_TIMEOUT.ms());
342             try {
343                 final CannedAugmentedFillResponse response;
344                 try {
345                     response = mResponses.poll(AUGMENTED_CONNECTION_TIMEOUT.ms(),
346                             TimeUnit.MILLISECONDS);
347                 } catch (InterruptedException e) {
348                     Log.w(TAG, "Interrupted getting CannedAugmentedFillResponse: " + e);
349                     Thread.currentThread().interrupt();
350                     addException(e);
351                     return;
352                 }
353                 if (response == null) {
354                     Log.w(TAG, "onFillRequest() for " + getActivityName(request)
355                             + " received when no canned response was set.");
356                     return;
357                 }
358 
359                 // sleep for timeout tests.
360                 final long delay = response.getDelay();
361                 if (delay > 0) {
362                     SystemClock.sleep(response.getDelay());
363                 }
364 
365                 if (response.getResponseType() == NULL) {
366                     Log.d(TAG, "onFillRequest(): replying with null");
367                     callback.onSuccess(null);
368                     return;
369                 }
370 
371                 if (response.getResponseType() == TIMEOUT) {
372                     Log.d(TAG, "onFillRequest(): not replying at all");
373                     return;
374                 }
375 
376                 Log.v(TAG, "onFillRequest(): response = " + response);
377                 final FillResponse fillResponse = response.asFillResponse(context, request,
378                         controller);
379                 Log.v(TAG, "onFillRequest(): fillResponse = " + fillResponse);
380                 callback.onSuccess(fillResponse);
381             } catch (Throwable t) {
382                 addException(t);
383             }
384         }
385     }
386 
387     public static final class ServiceWatcher {
388 
389         private final CountDownLatch mCreated = new CountDownLatch(1);
390         private final CountDownLatch mDestroyed = new CountDownLatch(1);
391 
392         private CtsAugmentedAutofillService mService;
393 
394         @NonNull
waitOnConnected()395         public CtsAugmentedAutofillService waitOnConnected() throws InterruptedException {
396             Log.d(TAG, "waitOnConnected(), mService= " + mService);
397             await(mCreated, "not created");
398 
399             if (mService == null) {
400                 throw new IllegalStateException("not created");
401             }
402 
403             return mService;
404         }
405 
waitOnDisconnected()406         public void waitOnDisconnected() throws InterruptedException {
407             Log.d(TAG, "waitOnConnected(), mService= " + mService);
408             await(mDestroyed, "not destroyed");
409         }
410 
411         @Override
toString()412         public String toString() {
413             return "mService: " + mService + " created: " + (mCreated.getCount() == 0)
414                     + " destroyed: " + (mDestroyed.getCount() == 0);
415         }
416     }
417 }
418