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