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