Home | History | Annotate | Download | only in cts
      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.dumpStructure;
     22 import static android.autofillservice.cts.Helper.getActivityName;
     23 import static android.autofillservice.cts.Timeouts.CONNECTION_TIMEOUT;
     24 import static android.autofillservice.cts.Timeouts.FILL_EVENTS_TIMEOUT;
     25 import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
     26 import static android.autofillservice.cts.Timeouts.IDLE_UNBIND_TIMEOUT;
     27 import static android.autofillservice.cts.Timeouts.SAVE_TIMEOUT;
     28 
     29 import static com.google.common.truth.Truth.assertThat;
     30 
     31 import android.app.assist.AssistStructure;
     32 import android.autofillservice.cts.CannedFillResponse.CannedDataset;
     33 import android.content.ComponentName;
     34 import android.content.IntentSender;
     35 import android.os.Bundle;
     36 import android.os.CancellationSignal;
     37 import android.os.SystemClock;
     38 import android.service.autofill.AutofillService;
     39 import android.service.autofill.Dataset;
     40 import android.service.autofill.FillCallback;
     41 import android.service.autofill.FillContext;
     42 import android.service.autofill.FillEventHistory;
     43 import android.service.autofill.FillEventHistory.Event;
     44 import android.service.autofill.FillResponse;
     45 import android.service.autofill.SaveCallback;
     46 import android.util.Log;
     47 
     48 import androidx.annotation.Nullable;
     49 
     50 import java.util.ArrayList;
     51 import java.util.List;
     52 import java.util.concurrent.BlockingQueue;
     53 import java.util.concurrent.LinkedBlockingQueue;
     54 import java.util.concurrent.TimeUnit;
     55 import java.util.concurrent.atomic.AtomicReference;
     56 
     57 /**
     58  * Implementation of {@link AutofillService} used in the tests.
     59  */
     60 public class InstrumentedAutoFillService extends AutofillService {
     61 
     62     static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
     63     static final String SERVICE_CLASS = "InstrumentedAutoFillService";
     64 
     65     static final String SERVICE_NAME = SERVICE_PACKAGE + "/." + SERVICE_CLASS;
     66 
     67     private static final String TAG = "InstrumentedAutoFillService";
     68 
     69     private static final boolean DUMP_FILL_REQUESTS = false;
     70     private static final boolean DUMP_SAVE_REQUESTS = false;
     71 
     72     protected static final AtomicReference<InstrumentedAutoFillService> sInstance =
     73             new AtomicReference<>();
     74     private static final Replier sReplier = new Replier();
     75 
     76     private static final Object sLock = new Object();
     77 
     78     // @GuardedBy("sLock") // NOTE: not using annotation because of dependencies
     79     private static boolean sIgnoreUnexpectedRequests = false;
     80 
     81     // @GuardedBy("sLock") // NOTE: not using annotation because of dependencies
     82     private static boolean sConnected;
     83 
     84     protected static String sServiceLabel = SERVICE_CLASS;
     85 
     86     public InstrumentedAutoFillService() {
     87         sInstance.set(this);
     88         sServiceLabel = SERVICE_CLASS;
     89     }
     90 
     91     private static InstrumentedAutoFillService peekInstance() {
     92         return sInstance.get();
     93     }
     94 
     95     /**
     96      * Gets the list of fill events in the {@link FillEventHistory}, waiting until it has the
     97      * expected size.
     98      */
     99     public static List<Event> getFillEvents(int expectedSize) throws Exception {
    100         final List<Event> events = getFillEventHistory(expectedSize).getEvents();
    101         // Sanity check
    102         if (expectedSize > 0 && events == null || events.size() != expectedSize) {
    103             throw new IllegalStateException("INTERNAL ERROR: events should have " + expectedSize
    104                     + ", but it is: " + events);
    105         }
    106         return events;
    107     }
    108 
    109     /**
    110      * Gets the {@link FillEventHistory}, waiting until it has the expected size.
    111      */
    112     public static FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
    113         final InstrumentedAutoFillService service = peekInstance();
    114 
    115         if (expectedSize == 0) {
    116             // Need to always sleep as there is no condition / callback to be used to wait until
    117             // expected number of events is set.
    118             SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
    119             final FillEventHistory history = service.getFillEventHistory();
    120             assertThat(history.getEvents()).isNull();
    121             return history;
    122         }
    123 
    124         return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
    125             final FillEventHistory history = service.getFillEventHistory();
    126             if (history == null) {
    127                 return null;
    128             }
    129             final List<Event> events = history.getEvents();
    130             if (events != null) {
    131                 if (events.size() != expectedSize) {
    132                     Log.v(TAG, "Didn't get " + expectedSize + " events yet: " + events);
    133                     return null;
    134                 }
    135             } else {
    136                 Log.v(TAG, "Events is still null (expecting " + expectedSize + ")");
    137                 return null;
    138             }
    139             return history;
    140         });
    141     }
    142 
    143     /**
    144      * Asserts there is no {@link FillEventHistory}.
    145      */
    146     public static void assertNoFillEventHistory() {
    147         // Need to always sleep as there is no condition / callback to be used to wait until
    148         // expected number of events is set.
    149         SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
    150         assertThat(peekInstance().getFillEventHistory()).isNull();
    151 
    152     }
    153 
    154     /**
    155      * Gets the service label associated with the current instance.
    156      */
    157     public static String getServiceLabel() {
    158         return sServiceLabel;
    159     }
    160 
    161     @Override
    162     public void onConnected() {
    163         synchronized (sLock) {
    164             Log.v(TAG, "onConnected(): connected=" + sConnected);
    165             sConnected = true;
    166         }
    167     }
    168 
    169     @Override
    170     public void onDisconnected() {
    171         synchronized (sLock) {
    172             Log.v(TAG, "onDisconnected(): connected=" + sConnected);
    173             sConnected = false;
    174         }
    175     }
    176 
    177     @Override
    178     public void onFillRequest(android.service.autofill.FillRequest request,
    179             CancellationSignal cancellationSignal, FillCallback callback) {
    180         if (DUMP_FILL_REQUESTS) dumpStructure("onFillRequest()", request.getFillContexts());
    181         synchronized (sLock) {
    182             if (sIgnoreUnexpectedRequests || !fromSamePackage(request.getFillContexts()))  {
    183                 Log.w(TAG, "Ignoring onFillRequest()");
    184                 return;
    185             }
    186         }
    187         sReplier.onFillRequest(request.getFillContexts(), request.getClientState(),
    188                 cancellationSignal, callback, request.getFlags());
    189     }
    190 
    191     @Override
    192     public void onSaveRequest(android.service.autofill.SaveRequest request,
    193             SaveCallback callback) {
    194         if (DUMP_SAVE_REQUESTS) dumpStructure("onSaveRequest()", request.getFillContexts());
    195         synchronized (sLock) {
    196             if (sIgnoreUnexpectedRequests || !fromSamePackage(request.getFillContexts())) {
    197                 Log.w(TAG, "Ignoring onSaveRequest()");
    198                 return;
    199             }
    200         }
    201         sReplier.onSaveRequest(request.getFillContexts(), request.getClientState(), callback,
    202                 request.getDatasetIds());
    203     }
    204 
    205     private static boolean isConnected() {
    206         synchronized (sLock) {
    207             return sConnected;
    208         }
    209     }
    210 
    211     private boolean fromSamePackage(List<FillContext> contexts) {
    212         final ComponentName component = contexts.get(contexts.size() - 1).getStructure()
    213                 .getActivityComponent();
    214         final String actualPackage = component.getPackageName();
    215         if (!actualPackage.equals(getPackageName())
    216                 && !actualPackage.equals(sReplier.mAcceptedPackageName)) {
    217             Log.w(TAG, "Got request from package " + actualPackage);
    218             return false;
    219         }
    220         return true;
    221     }
    222 
    223     /**
    224      * Sets whether unexpected calls to
    225      * {@link #onFillRequest(android.service.autofill.FillRequest, CancellationSignal, FillCallback)}
    226      * should throw an exception.
    227      */
    228     public static void setIgnoreUnexpectedRequests(boolean ignore) {
    229         synchronized (sLock) {
    230             sIgnoreUnexpectedRequests = ignore;
    231         }
    232     }
    233 
    234     /**
    235      * Waits until {@link #onConnected()} is called, or fails if it times out.
    236      *
    237      * <p>This method is useful on tests that explicitly verifies the connection, but should be
    238      * avoided in other tests, as it adds extra time to the test execution (and flakiness in cases
    239      * where the service might have being disconnected already; for example, if the fill request
    240      * was replied with a {@code null} response) - if a text needs to block until the service
    241      * receives a callback, it should use {@link Replier#getNextFillRequest()} instead.
    242      */
    243     static void waitUntilConnected() throws Exception {
    244         waitConnectionState(CONNECTION_TIMEOUT, true);
    245     }
    246 
    247     /**
    248      * Waits until {@link #onDisconnected()} is called, or fails if it times out.
    249      *
    250      * <p>This method is useful on tests that explicitly verifies the connection, but should be
    251      * avoided in other tests, as it adds extra time to the test execution.
    252      */
    253     static void waitUntilDisconnected() throws Exception {
    254         waitConnectionState(IDLE_UNBIND_TIMEOUT, false);
    255     }
    256 
    257     private static void waitConnectionState(Timeout timeout, boolean expected) throws Exception {
    258         timeout.run("wait for connected=" + expected,  () -> {
    259             return isConnected() == expected ? Boolean.TRUE : null;
    260         });
    261     }
    262 
    263     /**
    264      * Gets the {@link Replier} singleton.
    265      */
    266     static Replier getReplier() {
    267         return sReplier;
    268     }
    269 
    270     static void resetStaticState() {
    271         sInstance.set(null);
    272         sConnected = false;
    273         sServiceLabel = SERVICE_CLASS;
    274     }
    275 
    276     /**
    277      * POJO representation of the contents of a
    278      * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
    279      * CancellationSignal, FillCallback)} that can be asserted at the end of a test case.
    280      */
    281     static final class FillRequest {
    282         final AssistStructure structure;
    283         final List<FillContext> contexts;
    284         final Bundle data;
    285         final CancellationSignal cancellationSignal;
    286         final FillCallback callback;
    287         final int flags;
    288 
    289         private FillRequest(List<FillContext> contexts, Bundle data,
    290                 CancellationSignal cancellationSignal, FillCallback callback, int flags) {
    291             this.contexts = contexts;
    292             this.data = data;
    293             this.cancellationSignal = cancellationSignal;
    294             this.callback = callback;
    295             this.flags = flags;
    296             structure = contexts.get(contexts.size() - 1).getStructure();
    297         }
    298 
    299         @Override
    300         public String toString() {
    301             return "FillRequest:" + getActivityName(contexts);
    302         }
    303     }
    304 
    305     /**
    306      * POJO representation of the contents of a
    307      * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)}
    308      * that can be asserted at the end of a test case.
    309      */
    310     static final class SaveRequest {
    311         public final List<FillContext> contexts;
    312         public final AssistStructure structure;
    313         public final Bundle data;
    314         public final SaveCallback callback;
    315         public final List<String> datasetIds;
    316 
    317         private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
    318                 List<String> datasetIds) {
    319             if (contexts != null && contexts.size() > 0) {
    320                 structure = contexts.get(contexts.size() - 1).getStructure();
    321             } else {
    322                 structure = null;
    323             }
    324             this.contexts = contexts;
    325             this.data = data;
    326             this.callback = callback;
    327             this.datasetIds = datasetIds;
    328         }
    329 
    330         @Override
    331         public String toString() {
    332             return "SaveRequest:" + getActivityName(contexts);
    333         }
    334     }
    335 
    336     /**
    337      * Object used to answer a
    338      * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
    339      * CancellationSignal, FillCallback)}
    340      * on behalf of a unit test method.
    341      */
    342     static final class Replier {
    343 
    344         private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>();
    345         private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>();
    346         private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>();
    347 
    348         private List<Throwable> mExceptions;
    349         private IntentSender mOnSaveIntentSender;
    350         private String mAcceptedPackageName;
    351 
    352         private boolean mReportUnhandledFillRequest = true;
    353         private boolean mReportUnhandledSaveRequest = true;
    354 
    355         private Replier() {
    356         }
    357 
    358         private IdMode mIdMode = IdMode.RESOURCE_ID;
    359 
    360         public void setIdMode(IdMode mode) {
    361             this.mIdMode = mode;
    362         }
    363 
    364         public void acceptRequestsFromPackage(String packageName) {
    365             mAcceptedPackageName = packageName;
    366         }
    367 
    368         /**
    369          * Gets the exceptions thrown asynchronously, if any.
    370          */
    371         @Nullable List<Throwable> getExceptions() {
    372             return mExceptions;
    373         }
    374 
    375         private void addException(@Nullable Throwable e) {
    376             if (e == null) return;
    377 
    378             if (mExceptions == null) {
    379                 mExceptions = new ArrayList<>();
    380             }
    381             mExceptions.add(e);
    382         }
    383 
    384         /**
    385          * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just
    386          * one {@link Dataset}.
    387          */
    388         Replier addResponse(CannedDataset dataset) {
    389             return addResponse(new CannedFillResponse.Builder()
    390                     .addDataset(dataset)
    391                     .build());
    392         }
    393 
    394         /**
    395          * Sets the expectation for the next {@code onFillRequest}.
    396          */
    397         Replier addResponse(CannedFillResponse response) {
    398             if (response == null) {
    399                 throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
    400             }
    401             mResponses.add(response);
    402             return this;
    403         }
    404 
    405         /**
    406          * Sets the {@link IntentSender} that is passed to
    407          * {@link SaveCallback#onSuccess(IntentSender)}.
    408          */
    409         void setOnSave(IntentSender intentSender) {
    410             mOnSaveIntentSender = intentSender;
    411         }
    412 
    413         /**
    414          * Gets the next fill request, in the order received.
    415          *
    416          * <p>Typically called at the end of a test case, to assert the initial request.
    417          */
    418         FillRequest getNextFillRequest() {
    419             FillRequest request;
    420             try {
    421                 request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
    422             } catch (InterruptedException e) {
    423                 Thread.currentThread().interrupt();
    424                 throw new IllegalStateException("Interrupted", e);
    425             }
    426             if (request == null) {
    427                 throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called");
    428             }
    429             return request;
    430         }
    431 
    432         /**
    433          * Asserts that {@link #onFillRequest(List, Bundle, CancellationSignal, FillCallback, int)}
    434          * was not called.
    435          *
    436          * <p>Should only be called in cases where it's not expected to be called, as it will
    437          * sleep for a few ms.
    438          */
    439         void assertOnFillRequestNotCalled() {
    440             SystemClock.sleep(FILL_TIMEOUT.getMaxValue());
    441             assertThat(mFillRequests).isEmpty();
    442         }
    443 
    444         /**
    445          * Asserts all {@link AutofillService#onFillRequest(
    446          * android.service.autofill.FillRequest,  CancellationSignal, FillCallback) fill requests}
    447          * received by the service were properly {@link #getNextFillRequest() handled} by the test
    448          * case.
    449          */
    450         void assertNoUnhandledFillRequests() {
    451             if (mFillRequests.isEmpty()) return; // Good job, test case!
    452 
    453             if (!mReportUnhandledFillRequest) {
    454                 // Just log, so it's not thrown again on @After if already thrown on main body
    455                 Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
    456                         + "but logging just in case: " + mFillRequests);
    457                 return;
    458             }
    459 
    460             mReportUnhandledFillRequest = false;
    461             throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
    462                     + mFillRequests);
    463         }
    464 
    465         /**
    466          * Gets the current number of unhandled requests.
    467          */
    468         int getNumberUnhandledFillRequests() {
    469             return mFillRequests.size();
    470         }
    471 
    472         /**
    473          * Gets the next save request, in the order received.
    474          *
    475          * <p>Typically called at the end of a test case, to assert the initial request.
    476          */
    477         SaveRequest getNextSaveRequest() {
    478             SaveRequest request;
    479             try {
    480                 request = mSaveRequests.poll(SAVE_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
    481             } catch (InterruptedException e) {
    482                 Thread.currentThread().interrupt();
    483                 throw new IllegalStateException("Interrupted", e);
    484             }
    485             if (request == null) {
    486                 throw new RetryableException(SAVE_TIMEOUT, "onSaveRequest() not called");
    487             }
    488             return request;
    489         }
    490 
    491         /**
    492          * Asserts all
    493          * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)
    494          * save requests} received by the service were properly
    495          * {@link #getNextFillRequest() handled} by the test case.
    496          */
    497         void assertNoUnhandledSaveRequests() {
    498             if (mSaveRequests.isEmpty()) return; // Good job, test case!
    499 
    500             if (!mReportUnhandledSaveRequest) {
    501                 // Just log, so it's not thrown again on @After if already thrown on main body
    502                 Log.d(TAG, "assertNoUnhandledSaveRequests(): already reported, "
    503                         + "but logging just in case: " + mSaveRequests);
    504                 return;
    505             }
    506 
    507             mReportUnhandledSaveRequest = false;
    508             throw new AssertionError(mSaveRequests.size() + " unhandled save requests: "
    509                     + mSaveRequests);
    510         }
    511 
    512         /**
    513          * Resets its internal state.
    514          */
    515         void reset() {
    516             mResponses.clear();
    517             mFillRequests.clear();
    518             mSaveRequests.clear();
    519             mExceptions = null;
    520             mOnSaveIntentSender = null;
    521             mAcceptedPackageName = null;
    522             mReportUnhandledFillRequest = true;
    523             mReportUnhandledSaveRequest = true;
    524         }
    525 
    526         private void onFillRequest(List<FillContext> contexts, Bundle data,
    527                 CancellationSignal cancellationSignal, FillCallback callback, int flags) {
    528             try {
    529                 CannedFillResponse response = null;
    530                 try {
    531                     response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
    532                 } catch (InterruptedException e) {
    533                     Log.w(TAG, "Interrupted getting CannedResponse: " + e);
    534                     Thread.currentThread().interrupt();
    535                     addException(e);
    536                     return;
    537                 }
    538                 if (response == null) {
    539                     final String activityName = getActivityName(contexts);
    540                     final String msg = "onFillRequest() for activity " + activityName
    541                             + " received when no canned response was set.";
    542                     dumpStructure(msg, contexts);
    543                     return;
    544                 }
    545                 if (response.getResponseType() == NULL) {
    546                     Log.d(TAG, "onFillRequest(): replying with null");
    547                     callback.onSuccess(null);
    548                     return;
    549                 }
    550 
    551                 if (response.getResponseType() == TIMEOUT) {
    552                     Log.d(TAG, "onFillRequest(): not replying at all");
    553                     return;
    554                 }
    555 
    556                 final String failureMessage = response.getFailureMessage();
    557                 if (failureMessage != null) {
    558                     Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage);
    559                     callback.onFailure(failureMessage);
    560                     return;
    561                 }
    562 
    563                 final FillResponse fillResponse;
    564 
    565                 switch (mIdMode) {
    566                     case RESOURCE_ID:
    567                         fillResponse = response.asFillResponse(
    568                                 (id) -> Helper.findNodeByResourceId(contexts, id));
    569                         break;
    570                     case HTML_NAME:
    571                         fillResponse = response.asFillResponse(
    572                                 (name) -> Helper.findNodeByHtmlName(contexts, name));
    573                         break;
    574                     case HTML_NAME_OR_RESOURCE_ID:
    575                         fillResponse = response.asFillResponse(
    576                                 (id) -> Helper.findNodeByHtmlNameOrResourceId(contexts, id));
    577                         break;
    578                     default:
    579                         throw new IllegalStateException("Unknown id mode: " + mIdMode);
    580                 }
    581 
    582                 Log.v(TAG, "onFillRequest(): fillResponse = " + fillResponse);
    583                 callback.onSuccess(fillResponse);
    584             } catch (Throwable t) {
    585                 addException(t);
    586             } finally {
    587                 mFillRequests.offer(new FillRequest(contexts, data, cancellationSignal, callback,
    588                         flags));
    589             }
    590         }
    591 
    592         private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
    593                 List<String> datasetIds) {
    594             Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender);
    595             mSaveRequests.offer(new SaveRequest(contexts, data, callback, datasetIds));
    596             if (mOnSaveIntentSender != null) {
    597                 callback.onSuccess(mOnSaveIntentSender);
    598             } else {
    599                 callback.onSuccess();
    600             }
    601         }
    602     }
    603 }
    604