1 /*
2  * Copyright (C) 2018 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 package android.view.autofill;
17 
18 import android.app.assist.AssistStructure.ViewNode;
19 import android.os.CancellationSignal;
20 import android.service.autofill.AutofillService;
21 import android.service.autofill.Dataset;
22 import android.service.autofill.FillCallback;
23 import android.service.autofill.FillRequest;
24 import android.service.autofill.FillResponse;
25 import android.service.autofill.SaveCallback;
26 import android.service.autofill.SaveRequest;
27 import android.util.Log;
28 import android.util.Pair;
29 import android.widget.RemoteViews;
30 
31 import androidx.annotation.NonNull;
32 
33 import com.android.perftests.autofill.R;
34 
35 import java.util.concurrent.BlockingQueue;
36 import java.util.concurrent.LinkedBlockingQueue;
37 import java.util.concurrent.TimeUnit;
38 
39 /**
40  * An {@link AutofillService} implementation whose replies can be programmed by the test case.
41  */
42 public class MyAutofillService extends AutofillService {
43 
44     private static final String TAG = "MyAutofillService";
45     private static final int TIMEOUT_MS = 5_000;
46 
47     private static final String PACKAGE_NAME = "com.android.perftests.autofill";
48     static final String COMPONENT_NAME = PACKAGE_NAME + "/android.view.autofill.MyAutofillService";
49 
50     private static final BlockingQueue<FillRequest> sFillRequests = new LinkedBlockingQueue<>();
51     private static final BlockingQueue<CannedResponse> sCannedResponses =
52             new LinkedBlockingQueue<>();
53 
54     private static boolean sEnabled;
55 
56     /**
57      * Returns the TestWatcher that was used for the testing.
58      */
59     @NonNull
getTestWatcher()60     public static AutofillTestWatcher getTestWatcher() {
61         return new AutofillTestWatcher();
62     }
63 
64     /**
65      * Resets the static state associated with the service.
66      */
resetStaticState()67     static void resetStaticState() {
68         sFillRequests.clear();
69         sCannedResponses.clear();
70         sEnabled = false;
71     }
72 
73     /**
74      * Sets whether the service is enabled or not - when disabled, calls to
75      * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} will be ignored.
76      */
setEnabled(boolean enabled)77     static void setEnabled(boolean enabled) {
78         sEnabled = enabled;
79     }
80 
81     /**
82      * Gets the the last {@link FillRequest} passed to
83      * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} or throws an
84      * exception if that method was not called.
85      */
86     @NonNull
getLastFillRequest()87     static FillRequest getLastFillRequest() {
88         FillRequest request = null;
89         try {
90             request = sFillRequests.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
91         } catch (InterruptedException e) {
92             Thread.currentThread().interrupt();
93             throw new IllegalStateException("onFillRequest() interrupted");
94         }
95         if (request == null) {
96             throw new IllegalStateException("onFillRequest() not called in " + TIMEOUT_MS + "ms");
97         }
98         return request;
99     }
100 
101     @Override
onConnected()102     public void onConnected() {
103         AutofillTestWatcher.ServiceWatcher.onConnected();
104     }
105 
106     @Override
onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)107     public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
108             FillCallback callback) {
109         try {
110             handleRequest(request, callback);
111         } catch (InterruptedException e) {
112             Thread.currentThread().interrupt();
113             onError("onFillRequest() interrupted", e, callback);
114         } catch (Exception e) {
115             onError("exception on onFillRequest()", e, callback);
116         }
117     }
118 
119 
handleRequest(FillRequest request, FillCallback callback)120     private void handleRequest(FillRequest request, FillCallback callback) throws Exception {
121         if (!sEnabled) {
122             onError("ignoring onFillRequest(): service is disabled", callback);
123             return;
124         }
125         CannedResponse response = sCannedResponses.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
126         if (response == null) {
127             onError("ignoring onFillRequest(): response not set", callback);
128             return;
129         }
130         Dataset.Builder dataset = new Dataset.Builder(newDatasetPresentation("dataset"));
131         boolean hasData = false;
132         if (response.mUsername != null) {
133             hasData = true;
134             AutofillId autofillId = getAutofillIdByResourceId(request, response.mUsername.first);
135             AutofillValue value = AutofillValue.forText(response.mUsername.second);
136             dataset.setValue(autofillId, value, newDatasetPresentation("dataset"));
137         }
138         if (response.mPassword != null) {
139             hasData = true;
140             AutofillId autofillId = getAutofillIdByResourceId(request, response.mPassword.first);
141             AutofillValue value = AutofillValue.forText(response.mPassword.second);
142             dataset.setValue(autofillId, value, newDatasetPresentation("dataset"));
143         }
144         if (hasData) {
145             FillResponse.Builder fillResponse = new FillResponse.Builder();
146             if (response.mIgnoredIds != null) {
147                 int length = response.mIgnoredIds.length;
148                 AutofillId[] requiredIds = new AutofillId[length];
149                 for (int i = 0; i < length; i++) {
150                     String resourceId = response.mIgnoredIds[i];
151                     requiredIds[i] = getAutofillIdByResourceId(request, resourceId);
152                 }
153                 fillResponse.setIgnoredIds(requiredIds);
154             }
155             callback.onSuccess(fillResponse.addDataset(dataset.build()).build());
156         } else {
157             callback.onSuccess(null);
158         }
159         if (!sFillRequests.offer(request, TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
160             Log.w(TAG, "could not offer request in " + TIMEOUT_MS + "ms");
161         }
162     }
163 
getAutofillIdByResourceId(FillRequest request, String resourceId)164     private AutofillId getAutofillIdByResourceId(FillRequest request, String resourceId)
165             throws Exception {
166         ViewNode node = AutofillTestHelper.findNodeByResourceId(request.getFillContexts(),
167                 resourceId);
168         if (node == null) {
169             throw new AssertionError("No node with resource id " + resourceId);
170         }
171         return node.getAutofillId();
172     }
173 
174     @Override
onSaveRequest(SaveRequest request, SaveCallback callback)175     public void onSaveRequest(SaveRequest request, SaveCallback callback) {
176         // No current test should have triggered it...
177         Log.e(TAG, "onSaveRequest() should not have been called");
178         callback.onFailure("onSaveRequest() should not have been called");
179     }
180 
181     static final class CannedResponse {
182         private final Pair<String, String> mUsername;
183         private final Pair<String, String> mPassword;
184         private final String[] mIgnoredIds;
185 
CannedResponse(@onNull Builder builder)186         private CannedResponse(@NonNull Builder builder) {
187             mUsername = builder.mUsername;
188             mPassword = builder.mPassword;
189             mIgnoredIds = builder.mIgnoredIds;
190         }
191 
192         static class Builder {
193             private Pair<String, String> mUsername;
194             private Pair<String, String> mPassword;
195             private String[] mIgnoredIds;
196 
197             @NonNull
setUsername(@onNull String id, @NonNull String value)198             Builder setUsername(@NonNull String id, @NonNull String value) {
199                 mUsername = new Pair<>(id, value);
200                 return this;
201             }
202 
203             @NonNull
setPassword(@onNull String id, @NonNull String value)204             Builder setPassword(@NonNull String id, @NonNull String value) {
205                 mPassword = new Pair<>(id, value);
206                 return this;
207             }
208 
209             @NonNull
setIgnored(String... ids)210             Builder setIgnored(String... ids) {
211                 mIgnoredIds = ids;
212                 return this;
213             }
214 
reply()215             void reply() {
216                 sCannedResponses.add(new CannedResponse(this));
217             }
218         }
219     }
220 
221     /**
222      * Sets the expected canned {@link FillResponse} for the next
223      * {@link AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
224      */
newCannedResponse()225     static CannedResponse.Builder newCannedResponse() {
226         return new CannedResponse.Builder();
227     }
228 
onError(@onNull String msg, @NonNull FillCallback callback)229     private void onError(@NonNull String msg, @NonNull FillCallback callback) {
230         Log.e(TAG, msg);
231         callback.onFailure(msg);
232     }
233 
onError(@onNull String msg, @NonNull Exception e, @NonNull FillCallback callback)234     private void onError(@NonNull String msg, @NonNull Exception e,
235             @NonNull FillCallback callback) {
236         Log.e(TAG, msg, e);
237         callback.onFailure(msg);
238     }
239 
240     @NonNull
newDatasetPresentation(@onNull CharSequence text)241     private static RemoteViews newDatasetPresentation(@NonNull CharSequence text) {
242         RemoteViews presentation =
243                 new RemoteViews(PACKAGE_NAME, R.layout.autofill_dataset_picker_text_only);
244         presentation.setTextViewText(R.id.text, text);
245         return presentation;
246     }
247 }
248