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