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.NULL; 20 import static android.autofillservice.cts.CannedFillResponse.ResponseType.TIMEOUT; 21 import static android.autofillservice.cts.Helper.CONNECTION_TIMEOUT_MS; 22 import static android.autofillservice.cts.Helper.FILL_TIMEOUT_MS; 23 import static android.autofillservice.cts.Helper.IDLE_UNBIND_TIMEOUT_MS; 24 import static android.autofillservice.cts.Helper.SAVE_TIMEOUT_MS; 25 import static android.autofillservice.cts.Helper.dumpAutofillService; 26 import static android.autofillservice.cts.Helper.dumpStructure; 27 28 import static com.google.common.truth.Truth.assertWithMessage; 29 30 import android.app.assist.AssistStructure; 31 import android.autofillservice.cts.CannedFillResponse.CannedDataset; 32 import android.content.ComponentName; 33 import android.os.Bundle; 34 import android.os.CancellationSignal; 35 import android.service.autofill.AutofillService; 36 import android.service.autofill.Dataset; 37 import android.service.autofill.FillCallback; 38 import android.service.autofill.FillContext; 39 import android.service.autofill.FillResponse; 40 import android.service.autofill.SaveCallback; 41 import android.util.Log; 42 43 import java.util.List; 44 import java.util.concurrent.BlockingQueue; 45 import java.util.concurrent.LinkedBlockingQueue; 46 import java.util.concurrent.TimeUnit; 47 import java.util.concurrent.atomic.AtomicReference; 48 49 /** 50 * Implementation of {@link AutofillService} used in the tests. 51 */ 52 public class InstrumentedAutoFillService extends AutofillService { 53 54 private static final String TAG = "InstrumentedAutoFillService"; 55 56 private static final boolean DUMP_FILL_REQUESTS = false; 57 private static final boolean DUMP_SAVE_REQUESTS = false; 58 59 private static final String STATE_CONNECTED = "CONNECTED"; 60 private static final String STATE_DISCONNECTED = "DISCONNECTED"; 61 62 private static final AtomicReference<InstrumentedAutoFillService> sInstance = 63 new AtomicReference<>(); 64 private static final Replier sReplier = new Replier(); 65 private static final BlockingQueue<String> sConnectionStates = new LinkedBlockingQueue<>(); 66 67 private static boolean sIgnoreUnexpectedRequests = false; 68 69 public InstrumentedAutoFillService() { 70 sInstance.set(this); 71 } 72 73 public static AutofillService peekInstance() { 74 return sInstance.get(); 75 } 76 77 @Override 78 public void onConnected() { 79 Log.v(TAG, "onConnected(): " + sConnectionStates); 80 sConnectionStates.offer(STATE_CONNECTED); 81 } 82 83 @Override 84 public void onDisconnected() { 85 Log.v(TAG, "onDisconnected(): " + sConnectionStates); 86 sConnectionStates.offer(STATE_DISCONNECTED); 87 } 88 89 @Override 90 public void onFillRequest(android.service.autofill.FillRequest request, 91 CancellationSignal cancellationSignal, FillCallback callback) { 92 if (sIgnoreUnexpectedRequests || !fromSamePackage(request.getFillContexts())) { 93 Log.w(TAG, "Ignoring onFillRequest()"); 94 return; 95 } 96 if (DUMP_FILL_REQUESTS) dumpStructure("onFillRequest()", request.getFillContexts()); 97 sReplier.onFillRequest(request.getFillContexts(), request.getClientState(), 98 cancellationSignal, callback, request.getFlags()); 99 } 100 101 @Override 102 public void onSaveRequest(android.service.autofill.SaveRequest request, 103 SaveCallback callback) { 104 if (sIgnoreUnexpectedRequests || !fromSamePackage(request.getFillContexts())) { 105 Log.w(TAG, "Ignoring onSaveRequest()"); 106 return; 107 } 108 if (DUMP_SAVE_REQUESTS) dumpStructure("onSaveRequest()", request.getFillContexts()); 109 sReplier.onSaveRequest(request.getFillContexts(), request.getClientState(), callback); 110 } 111 112 private boolean fromSamePackage(List<FillContext> contexts) { 113 final ComponentName component = contexts.get(contexts.size() - 1).getStructure() 114 .getActivityComponent(); 115 final String actualPackage = component.getPackageName(); 116 if (!actualPackage.equals(getPackageName())) { 117 Log.w(TAG, "Got request from package " + actualPackage); 118 return false; 119 } 120 return true; 121 } 122 123 /** 124 * Sets whether unexpected calls to 125 * {@link #onFillRequest(android.service.autofill.FillRequest, CancellationSignal, FillCallback)} 126 * should throw an exception. 127 */ 128 public static void setIgnoreUnexpectedRequests(boolean ignore) { 129 sIgnoreUnexpectedRequests = ignore; 130 } 131 132 /** 133 * Waits until {@link #onConnected()} is called, or fails if it times out. 134 * 135 * <p>This method is useful on tests that explicitly verifies the connection, but should be 136 * avoided in other tests, as it adds extra time to the test execution - if a text needs to 137 * block until the service receives a callback, it should use 138 * {@link Replier#getNextFillRequest()} instead. 139 */ 140 static void waitUntilConnected() throws InterruptedException { 141 final String state = sConnectionStates.poll(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS); 142 if (state == null) { 143 dumpAutofillService(); 144 throw new RetryableException("not connected in %d ms", CONNECTION_TIMEOUT_MS); 145 } 146 assertWithMessage("Invalid connection state").that(state).isEqualTo(STATE_CONNECTED); 147 } 148 149 /** 150 * Waits until {@link #onDisconnected()} is called, or fails if it times out. 151 * 152 * <p>This method is useful on tests that explicitly verifies the connection, but should be 153 * avoided in other tests, as it adds extra time to the test execution. 154 */ 155 static void waitUntilDisconnected() throws InterruptedException { 156 final String state = sConnectionStates.poll(2 * IDLE_UNBIND_TIMEOUT_MS, 157 TimeUnit.MILLISECONDS); 158 if (state == null) { 159 throw new RetryableException("not disconnected in %d ms", IDLE_UNBIND_TIMEOUT_MS); 160 } 161 assertWithMessage("Invalid connection state").that(state).isEqualTo(STATE_DISCONNECTED); 162 } 163 164 /** 165 * Gets the {@link Replier} singleton. 166 */ 167 static Replier getReplier() { 168 return sReplier; 169 } 170 171 static void resetStaticState() { 172 sConnectionStates.clear(); 173 } 174 175 /** 176 * POJO representation of the contents of a 177 * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest, 178 * CancellationSignal, FillCallback)} that can be asserted at the end of a test case. 179 */ 180 static final class FillRequest { 181 final AssistStructure structure; 182 final List<FillContext> contexts; 183 final Bundle data; 184 final CancellationSignal cancellationSignal; 185 final FillCallback callback; 186 final int flags; 187 188 private FillRequest(List<FillContext> contexts, Bundle data, 189 CancellationSignal cancellationSignal, FillCallback callback, int flags) { 190 this.contexts = contexts; 191 this.data = data; 192 this.cancellationSignal = cancellationSignal; 193 this.callback = callback; 194 this.flags = flags; 195 structure = contexts.get(contexts.size() - 1).getStructure(); 196 } 197 } 198 199 /** 200 * POJO representation of the contents of a 201 * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)} 202 * that can be asserted at the end of a test case. 203 */ 204 static final class SaveRequest { 205 final List<FillContext> contexts; 206 final AssistStructure structure; 207 final Bundle data; 208 final SaveCallback callback; 209 210 private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback) { 211 if (contexts != null && contexts.size() > 0) { 212 structure = contexts.get(contexts.size() - 1).getStructure(); 213 } else { 214 structure = null; 215 } 216 this.contexts = contexts; 217 this.data = data; 218 this.callback = callback; 219 } 220 } 221 222 /** 223 * Object used to answer a 224 * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest, 225 * CancellationSignal, FillCallback)} 226 * on behalf of a unit test method. 227 */ 228 static final class Replier { 229 230 private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>(); 231 private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>(); 232 private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>(); 233 234 private Replier() { 235 } 236 237 /** 238 * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just 239 * one {@link Dataset}. 240 */ 241 Replier addResponse(CannedDataset dataset) { 242 return addResponse(new CannedFillResponse.Builder() 243 .addDataset(dataset) 244 .build()); 245 } 246 247 /** 248 * Sets the expectation for the next {@code onFillRequest}. 249 */ 250 Replier addResponse(CannedFillResponse response) { 251 if (response == null) { 252 throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead"); 253 } 254 mResponses.add(response); 255 return this; 256 } 257 258 /** 259 * Gets the next fill request, in the order received. 260 * 261 * <p>Typically called at the end of a test case, to assert the initial request. 262 */ 263 FillRequest getNextFillRequest() throws InterruptedException { 264 final FillRequest request = mFillRequests.poll(FILL_TIMEOUT_MS, TimeUnit.MILLISECONDS); 265 if (request == null) { 266 throw new RetryableException("onFillRequest() not called in %s ms", 267 FILL_TIMEOUT_MS); 268 } 269 return request; 270 } 271 272 /** 273 * Asserts the total number of {@link AutofillService#onFillRequest( 274 * android.service.autofill.FillRequest, CancellationSignal, FillCallback)}, minus those 275 * returned by {@link #getNextFillRequest()}. 276 */ 277 void assertNumberUnhandledFillRequests(int expected) { 278 assertWithMessage("Invalid number of fill requests").that(mFillRequests.size()) 279 .isEqualTo(expected); 280 } 281 282 /** 283 * Gets the next save request, in the order received. 284 * 285 * <p>Typically called at the end of a test case, to assert the initial request. 286 */ 287 SaveRequest getNextSaveRequest() throws InterruptedException { 288 final SaveRequest request = mSaveRequests.poll(SAVE_TIMEOUT_MS, TimeUnit.MILLISECONDS); 289 if (request == null) { 290 throw new RetryableException( 291 "onSaveRequest() not called in %d ms", SAVE_TIMEOUT_MS); 292 } 293 return request; 294 } 295 296 /** 297 * Asserts the total number of 298 * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)} 299 * minus those returned by {@link #getNextSaveRequest()}. 300 */ 301 void assertNumberUnhandledSaveRequests(int expected) { 302 assertWithMessage("Invalid number of save requests").that(mSaveRequests.size()) 303 .isEqualTo(expected); 304 } 305 306 /** 307 * Resets its internal state. 308 */ 309 void reset() { 310 mResponses.clear(); 311 mFillRequests.clear(); 312 mSaveRequests.clear(); 313 } 314 315 private void onFillRequest(List<FillContext> contexts, Bundle data, 316 CancellationSignal cancellationSignal, FillCallback callback, int flags) { 317 try { 318 CannedFillResponse response = null; 319 try { 320 response = mResponses.poll(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS); 321 } catch (InterruptedException e) { 322 Log.w(TAG, "Interrupted getting CannedResponse: " + e); 323 Thread.currentThread().interrupt(); 324 return; 325 } 326 if (response == null) { 327 dumpStructure("onFillRequest() without response", contexts); 328 throw new RetryableException("No CannedResponse"); 329 } 330 if (response.getResponseType() == NULL) { 331 Log.d(TAG, "onFillRequest(): replying with null"); 332 callback.onSuccess(null); 333 return; 334 } 335 336 if (response.getResponseType() == TIMEOUT) { 337 Log.d(TAG, "onFillRequest(): not replying at all"); 338 return; 339 } 340 341 final String failureMessage = response.getFailureMessage(); 342 if (failureMessage != null) { 343 Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage); 344 callback.onFailure(failureMessage); 345 return; 346 } 347 348 final FillResponse fillResponse = response.asFillResponse( 349 (id) -> Helper.findNodeByResourceId(contexts, id)); 350 351 Log.v(TAG, "onFillRequest(): fillResponse = " + fillResponse); 352 callback.onSuccess(fillResponse); 353 } finally { 354 mFillRequests.offer(new FillRequest(contexts, data, cancellationSignal, callback, 355 flags)); 356 } 357 } 358 359 private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback) { 360 Log.d(TAG, "onSaveRequest()"); 361 mSaveRequests.offer(new SaveRequest(contexts, data, callback)); 362 callback.onSuccess(); 363 } 364 } 365 } 366