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