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 com.example.android.autofill.service.simple;
17 
18 import static com.example.android.autofill.service.simple.BasicService.getLatestAssistStructure;
19 import static com.example.android.autofill.service.simple.BasicService.newDatasetPresentation;
20 
21 import android.app.assist.AssistStructure;
22 import android.app.assist.AssistStructure.ViewNode;
23 import android.content.Context;
24 import android.content.IntentSender;
25 import android.os.CancellationSignal;
26 import android.service.autofill.AutofillService;
27 import android.service.autofill.Dataset;
28 import android.service.autofill.FillCallback;
29 import android.service.autofill.FillRequest;
30 import android.service.autofill.FillResponse;
31 import android.service.autofill.SaveCallback;
32 import android.service.autofill.SaveInfo;
33 import android.service.autofill.SaveRequest;
34 import android.support.annotation.NonNull;
35 import android.support.annotation.Nullable;
36 import android.text.TextUtils;
37 import android.util.ArrayMap;
38 import android.util.Log;
39 import android.view.View;
40 import android.view.autofill.AutofillId;
41 import android.view.autofill.AutofillValue;
42 import android.widget.RemoteViews;
43 import android.widget.Toast;
44 
45 import com.example.android.autofill.service.MyAutofillService;
46 import com.example.android.autofill.service.settings.MyPreferences;
47 
48 import java.util.Collection;
49 import java.util.Map;
50 import java.util.Map.Entry;
51 
52 /**
53  * A basic service that provides autofill data for pretty much any input field, even those not
54  * annotated with autfoill hints.
55  *
56  * <p>The goal of this class is to provide a simple autofill service implementation that can be used
57  * to debug how other apps interact with autofill, it should <strong>not</strong> be used as a
58  * reference for real autofill service implementations because it lacks fundamental security
59  * requirements such as data partitioning and package verification &mdashthese requirements are
60  * fullfilled by {@link MyAutofillService}.
61  */
62 public class DebugService extends AutofillService {
63 
64     private static final String TAG = "DebugService";
65 
66     private boolean mAuthenticateResponses;
67     private boolean mAuthenticateDatasets;
68     private int mNumberDatasets;
69 
70     @Override
onConnected()71     public void onConnected() {
72         super.onConnected();
73 
74         // TODO(b/114236837): use its own preferences?
75         MyPreferences pref = MyPreferences.getInstance(getApplicationContext());
76         mAuthenticateResponses = pref.isResponseAuth();
77         mAuthenticateDatasets = pref.isDatasetAuth();
78         mNumberDatasets = pref.getNumberDatasets(4);
79 
80         Log.d(TAG, "onConnected(): numberDatasets=" + mNumberDatasets
81                 + ", authResponses=" + mAuthenticateResponses
82                 + ", authDatasets=" + mAuthenticateDatasets);
83     }
84 
85     @Override
onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)86     public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
87             FillCallback callback) {
88         Log.d(TAG, "onFillRequest()");
89 
90         // Find autofillable fields
91         AssistStructure structure = getLatestAssistStructure(request);
92         ArrayMap<String, AutofillId> fields = getAutofillableFields(structure);
93         Log.d(TAG, "autofillable fields:" + fields);
94 
95         if (fields.isEmpty()) {
96             toast("No autofill hints found");
97             callback.onSuccess(null);
98             return;
99         }
100 
101         // Create response...
102         FillResponse response;
103         if (mAuthenticateResponses) {
104             int size = fields.size();
105             String[] hints = new String[size];
106             AutofillId[] ids = new AutofillId[size];
107             for (int i = 0; i < size; i++) {
108                 hints[i] = fields.keyAt(i);
109                 ids[i] = fields.valueAt(i);
110             }
111 
112             IntentSender authentication = SimpleAuthActivity.newIntentSenderForResponse(this, hints,
113                     ids, mAuthenticateDatasets);
114             RemoteViews presentation = newDatasetPresentation(getPackageName(),
115                         "Tap to auth response");
116 
117             response = new FillResponse.Builder()
118                     .setAuthentication(ids, authentication, presentation).build();
119         } else {
120             response = createResponse(this, fields, mNumberDatasets,mAuthenticateDatasets);
121         }
122 
123         // ... and return it
124         callback.onSuccess(response);
125     }
126 
127     @Override
onSaveRequest(SaveRequest request, SaveCallback callback)128     public void onSaveRequest(SaveRequest request, SaveCallback callback) {
129         Log.d(TAG, "onSaveRequest()");
130         toast("Save not supported");
131         callback.onSuccess();
132     }
133 
134     /**
135      * Parses the {@link AssistStructure} representing the activity being autofilled, and returns a
136      * map of autofillable fields (represented by their autofill ids) mapped by the hint associate
137      * with them.
138      *
139      * <p>An autofillable field is a {@link ViewNode} whose {@link #getHint(ViewNode)} metho
140      */
141     @NonNull
getAutofillableFields(@onNull AssistStructure structure)142     private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) {
143         ArrayMap<String, AutofillId> fields = new ArrayMap<>();
144         int nodes = structure.getWindowNodeCount();
145         for (int i = 0; i < nodes; i++) {
146             ViewNode node = structure.getWindowNodeAt(i).getRootViewNode();
147             addAutofillableFields(fields, node);
148         }
149         return fields;
150     }
151 
152     /**
153      * Adds any autofillable view from the {@link ViewNode} and its descendants to the map.
154      */
addAutofillableFields(@onNull Map<String, AutofillId> fields, @NonNull ViewNode node)155     private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
156             @NonNull ViewNode node) {
157         String hint = getHint(node);
158         if (hint != null) {
159             AutofillId id = node.getAutofillId();
160             if (!fields.containsKey(hint)) {
161                 Log.v(TAG, "Setting hint '" + hint + "' on " + id);
162                 fields.put(hint, id);
163             } else {
164                 Log.v(TAG, "Ignoring hint '" + hint + "' on " + id
165                         + " because it was already set");
166             }
167         }
168         int childrenSize = node.getChildCount();
169         for (int i = 0; i < childrenSize; i++) {
170             addAutofillableFields(fields, node.getChildAt(i));
171         }
172     }
173 
174     @Nullable
getHint(@onNull ViewNode node)175     protected String getHint(@NonNull ViewNode node) {
176 
177         // First try the explicit autofill hints...
178 
179         String[] hints = node.getAutofillHints();
180         if (hints != null) {
181             // We're simple, we only care about the first hint
182             return hints[0].toLowerCase();
183         }
184 
185         // Then try some rudimentary heuristics based on other node properties
186 
187         String viewHint = node.getHint();
188         String hint = inferHint(node, viewHint);
189         if (hint != null) {
190             Log.d(TAG, "Found hint using view hint(" + viewHint + "): " + hint);
191             return hint;
192         } else if (!TextUtils.isEmpty(viewHint)) {
193             Log.v(TAG, "No hint using view hint: " + viewHint);
194         }
195 
196         String resourceId = node.getIdEntry();
197         hint = inferHint(node, resourceId);
198         if (hint != null) {
199             Log.d(TAG, "Found hint using resourceId(" + resourceId + "): " + hint);
200             return hint;
201         } else if (!TextUtils.isEmpty(resourceId)) {
202             Log.v(TAG, "No hint using resourceId: " + resourceId);
203         }
204 
205         CharSequence text = node.getText();
206         CharSequence className = node.getClassName();
207         if (text != null && className != null && className.toString().contains("EditText")) {
208             hint = inferHint(node, text.toString());
209             if (hint != null) {
210                 // NODE: text should not be logged, as it could contain PII
211                 Log.d(TAG, "Found hint using text(" + text + "): " + hint);
212                 return hint;
213             }
214         } else if (!TextUtils.isEmpty(text)) {
215             // NODE: text should not be logged, as it could contain PII
216             Log.v(TAG, "No hint using text: " + text + " and class " + className);
217         }
218         return null;
219     }
220 
221     /**
222      * Uses heuristics to infer an autofill hint from a {@code string}.
223      *
224      * @return standard autofill hint, or {@code null} when it could not be inferred.
225      */
226     @Nullable
inferHint(ViewNode node, @Nullable String actualHint)227     protected String inferHint(ViewNode node, @Nullable String actualHint) {
228         if (actualHint == null) return null;
229 
230         String hint = actualHint.toLowerCase();
231         if (hint.contains("label") || hint.contains("container")) {
232             Log.v(TAG, "Ignoring 'label/container' hint: " + hint);
233             return null;
234         }
235 
236         if (hint.contains("password")) return View.AUTOFILL_HINT_PASSWORD;
237         if (hint.contains("username")
238                 || (hint.contains("login") && hint.contains("id")))
239             return View.AUTOFILL_HINT_USERNAME;
240         if (hint.contains("email")) return View.AUTOFILL_HINT_EMAIL_ADDRESS;
241         if (hint.contains("name")) return View.AUTOFILL_HINT_NAME;
242         if (hint.contains("phone")) return View.AUTOFILL_HINT_PHONE;
243 
244         // When everything else fails, return the full string - this is helpful to help app
245         // developers visualize when autofill is triggered when it shouldn't (for example, in a
246         // chat conversation window), so they can mark the root view of such activities with
247         // android:importantForAutofill=noExcludeDescendants
248         if (node.isEnabled() && node.getAutofillType() != View.AUTOFILL_TYPE_NONE) {
249             Log.v(TAG, "Falling back to " + actualHint);
250             return actualHint;
251         }
252         return null;
253     }
254 
createResponse(@onNull Context context, @NonNull ArrayMap<String, AutofillId> fields, int numDatasets, boolean authenticateDatasets)255     static FillResponse createResponse(@NonNull Context context,
256             @NonNull ArrayMap<String, AutofillId> fields, int numDatasets,
257             boolean authenticateDatasets) {
258         String packageName = context.getPackageName();
259         FillResponse.Builder response = new FillResponse.Builder();
260         // 1.Add the dynamic datasets
261         for (int i = 1; i <= numDatasets; i++) {
262             Dataset unlockedDataset = newUnlockedDataset(fields, packageName, i);
263             if (authenticateDatasets) {
264                 Dataset.Builder lockedDataset = new Dataset.Builder();
265                 for (Entry<String, AutofillId> field : fields.entrySet()) {
266                     String hint = field.getKey();
267                     AutofillId id = field.getValue();
268                     String value = i + "-" + hint;
269                     IntentSender authentication =
270                             SimpleAuthActivity.newIntentSenderForDataset(context, unlockedDataset);
271                     RemoteViews presentation = newDatasetPresentation(packageName,
272                             "Tap to auth " + value);
273                     lockedDataset.setValue(id, null, presentation)
274                             .setAuthentication(authentication);
275                 }
276                 response.addDataset(lockedDataset.build());
277             } else {
278                 response.addDataset(unlockedDataset);
279             }
280         }
281 
282         // 2.Add save info
283         Collection<AutofillId> ids = fields.values();
284         AutofillId[] requiredIds = new AutofillId[ids.size()];
285         ids.toArray(requiredIds);
286         response.setSaveInfo(
287                 // We're simple, so we're generic
288                 new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build());
289 
290         // 3.Profit!
291         return response.build();
292     }
293 
newUnlockedDataset(@onNull Map<String, AutofillId> fields, @NonNull String packageName, int i)294     static Dataset newUnlockedDataset(@NonNull Map<String, AutofillId> fields,
295             @NonNull String packageName, int i) {
296         Dataset.Builder dataset = new Dataset.Builder();
297         for (Entry<String, AutofillId> field : fields.entrySet()) {
298             String hint = field.getKey();
299             AutofillId id = field.getValue();
300             String value = i + "-" + hint;
301 
302             // We're simple - our dataset values are hardcoded as "N-hint" (for example,
303             // "1-username", "2-username") and they're displayed as such, except if they're a
304             // password
305             String displayValue = hint.contains("password") ? "password for #" + i : value;
306             RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
307             dataset.setValue(id, AutofillValue.forText(value), presentation);
308         }
309 
310         return dataset.build();
311     }
312 
313     /**
314      * Displays a toast with the given message.
315      */
toast(@onNull CharSequence message)316     private void toast(@NonNull CharSequence message) {
317         Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
318     }
319 }
320