| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.example.android.autofill.service.simple; |
| |
| import static com.example.android.autofill.service.simple.BasicService.getLatestAssistStructure; |
| import static com.example.android.autofill.service.simple.BasicService.newDatasetPresentation; |
| |
| import android.app.assist.AssistStructure; |
| import android.app.assist.AssistStructure.ViewNode; |
| import android.content.Context; |
| import android.content.IntentSender; |
| import android.os.CancellationSignal; |
| import android.service.autofill.AutofillService; |
| import android.service.autofill.Dataset; |
| import android.service.autofill.FillCallback; |
| import android.service.autofill.FillRequest; |
| import android.service.autofill.FillResponse; |
| import android.service.autofill.SaveCallback; |
| import android.service.autofill.SaveInfo; |
| import android.service.autofill.SaveRequest; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.autofill.AutofillId; |
| import android.view.autofill.AutofillValue; |
| import android.widget.RemoteViews; |
| import android.widget.Toast; |
| |
| import com.example.android.autofill.service.MyAutofillService; |
| import com.example.android.autofill.service.settings.MyPreferences; |
| |
| import java.util.Collection; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| /** |
| * A basic service that provides autofill data for pretty much any input field, even those not |
| * annotated with autfoill hints. |
| * |
| * <p>The goal of this class is to provide a simple autofill service implementation that can be used |
| * to debug how other apps interact with autofill, it should <strong>not</strong> be used as a |
| * reference for real autofill service implementations because it lacks fundamental security |
| * requirements such as data partitioning and package verification &mdashthese requirements are |
| * fullfilled by {@link MyAutofillService}. |
| */ |
| public class DebugService extends AutofillService { |
| |
| private static final String TAG = "DebugService"; |
| |
| private boolean mAuthenticateResponses; |
| private boolean mAuthenticateDatasets; |
| private int mNumberDatasets; |
| |
| @Override |
| public void onConnected() { |
| super.onConnected(); |
| |
| // TODO(b/114236837): use its own preferences? |
| MyPreferences pref = MyPreferences.getInstance(getApplicationContext()); |
| mAuthenticateResponses = pref.isResponseAuth(); |
| mAuthenticateDatasets = pref.isDatasetAuth(); |
| mNumberDatasets = pref.getNumberDatasets(4); |
| |
| Log.d(TAG, "onConnected(): numberDatasets=" + mNumberDatasets |
| + ", authResponses=" + mAuthenticateResponses |
| + ", authDatasets=" + mAuthenticateDatasets); |
| } |
| |
| @Override |
| public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, |
| FillCallback callback) { |
| Log.d(TAG, "onFillRequest()"); |
| |
| // Find autofillable fields |
| AssistStructure structure = getLatestAssistStructure(request); |
| ArrayMap<String, AutofillId> fields = getAutofillableFields(structure); |
| Log.d(TAG, "autofillable fields:" + fields); |
| |
| if (fields.isEmpty()) { |
| toast("No autofill hints found"); |
| callback.onSuccess(null); |
| return; |
| } |
| |
| // Create response... |
| FillResponse response; |
| if (mAuthenticateResponses) { |
| int size = fields.size(); |
| String[] hints = new String[size]; |
| AutofillId[] ids = new AutofillId[size]; |
| for (int i = 0; i < size; i++) { |
| hints[i] = fields.keyAt(i); |
| ids[i] = fields.valueAt(i); |
| } |
| |
| IntentSender authentication = SimpleAuthActivity.newIntentSenderForResponse(this, hints, |
| ids, mAuthenticateDatasets); |
| RemoteViews presentation = newDatasetPresentation(getPackageName(), |
| "Tap to auth response"); |
| |
| response = new FillResponse.Builder() |
| .setAuthentication(ids, authentication, presentation).build(); |
| } else { |
| response = createResponse(this, fields, mNumberDatasets,mAuthenticateDatasets); |
| } |
| |
| // ... and return it |
| callback.onSuccess(response); |
| } |
| |
| @Override |
| public void onSaveRequest(SaveRequest request, SaveCallback callback) { |
| Log.d(TAG, "onSaveRequest()"); |
| toast("Save not supported"); |
| callback.onSuccess(); |
| } |
| |
| /** |
| * Parses the {@link AssistStructure} representing the activity being autofilled, and returns a |
| * map of autofillable fields (represented by their autofill ids) mapped by the hint associate |
| * with them. |
| * |
| * <p>An autofillable field is a {@link ViewNode} whose {@link #getHint(ViewNode)} metho |
| */ |
| @NonNull |
| private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) { |
| ArrayMap<String, AutofillId> fields = new ArrayMap<>(); |
| int nodes = structure.getWindowNodeCount(); |
| for (int i = 0; i < nodes; i++) { |
| ViewNode node = structure.getWindowNodeAt(i).getRootViewNode(); |
| addAutofillableFields(fields, node); |
| } |
| return fields; |
| } |
| |
| /** |
| * Adds any autofillable view from the {@link ViewNode} and its descendants to the map. |
| */ |
| private void addAutofillableFields(@NonNull Map<String, AutofillId> fields, |
| @NonNull ViewNode node) { |
| String hint = getHint(node); |
| if (hint != null) { |
| AutofillId id = node.getAutofillId(); |
| if (!fields.containsKey(hint)) { |
| Log.v(TAG, "Setting hint '" + hint + "' on " + id); |
| fields.put(hint, id); |
| } else { |
| Log.v(TAG, "Ignoring hint '" + hint + "' on " + id |
| + " because it was already set"); |
| } |
| } |
| int childrenSize = node.getChildCount(); |
| for (int i = 0; i < childrenSize; i++) { |
| addAutofillableFields(fields, node.getChildAt(i)); |
| } |
| } |
| |
| @Nullable |
| protected String getHint(@NonNull ViewNode node) { |
| |
| // First try the explicit autofill hints... |
| |
| String[] hints = node.getAutofillHints(); |
| if (hints != null) { |
| // We're simple, we only care about the first hint |
| return hints[0].toLowerCase(); |
| } |
| |
| // Then try some rudimentary heuristics based on other node properties |
| |
| String viewHint = node.getHint(); |
| String hint = inferHint(node, viewHint); |
| if (hint != null) { |
| Log.d(TAG, "Found hint using view hint(" + viewHint + "): " + hint); |
| return hint; |
| } else if (!TextUtils.isEmpty(viewHint)) { |
| Log.v(TAG, "No hint using view hint: " + viewHint); |
| } |
| |
| String resourceId = node.getIdEntry(); |
| hint = inferHint(node, resourceId); |
| if (hint != null) { |
| Log.d(TAG, "Found hint using resourceId(" + resourceId + "): " + hint); |
| return hint; |
| } else if (!TextUtils.isEmpty(resourceId)) { |
| Log.v(TAG, "No hint using resourceId: " + resourceId); |
| } |
| |
| CharSequence text = node.getText(); |
| CharSequence className = node.getClassName(); |
| if (text != null && className != null && className.toString().contains("EditText")) { |
| hint = inferHint(node, text.toString()); |
| if (hint != null) { |
| // NODE: text should not be logged, as it could contain PII |
| Log.d(TAG, "Found hint using text(" + text + "): " + hint); |
| return hint; |
| } |
| } else if (!TextUtils.isEmpty(text)) { |
| // NODE: text should not be logged, as it could contain PII |
| Log.v(TAG, "No hint using text: " + text + " and class " + className); |
| } |
| return null; |
| } |
| |
| /** |
| * Uses heuristics to infer an autofill hint from a {@code string}. |
| * |
| * @return standard autofill hint, or {@code null} when it could not be inferred. |
| */ |
| @Nullable |
| protected String inferHint(ViewNode node, @Nullable String actualHint) { |
| if (actualHint == null) return null; |
| |
| String hint = actualHint.toLowerCase(); |
| if (hint.contains("label") || hint.contains("container")) { |
| Log.v(TAG, "Ignoring 'label/container' hint: " + hint); |
| return null; |
| } |
| |
| if (hint.contains("password")) return View.AUTOFILL_HINT_PASSWORD; |
| if (hint.contains("username") |
| || (hint.contains("login") && hint.contains("id"))) |
| return View.AUTOFILL_HINT_USERNAME; |
| if (hint.contains("email")) return View.AUTOFILL_HINT_EMAIL_ADDRESS; |
| if (hint.contains("name")) return View.AUTOFILL_HINT_NAME; |
| if (hint.contains("phone")) return View.AUTOFILL_HINT_PHONE; |
| |
| // When everything else fails, return the full string - this is helpful to help app |
| // developers visualize when autofill is triggered when it shouldn't (for example, in a |
| // chat conversation window), so they can mark the root view of such activities with |
| // android:importantForAutofill=noExcludeDescendants |
| if (node.isEnabled() && node.getAutofillType() != View.AUTOFILL_TYPE_NONE) { |
| Log.v(TAG, "Falling back to " + actualHint); |
| return actualHint; |
| } |
| return null; |
| } |
| |
| static FillResponse createResponse(@NonNull Context context, |
| @NonNull ArrayMap<String, AutofillId> fields, int numDatasets, |
| boolean authenticateDatasets) { |
| String packageName = context.getPackageName(); |
| FillResponse.Builder response = new FillResponse.Builder(); |
| // 1.Add the dynamic datasets |
| for (int i = 1; i <= numDatasets; i++) { |
| Dataset unlockedDataset = newUnlockedDataset(fields, packageName, i); |
| if (authenticateDatasets) { |
| Dataset.Builder lockedDataset = new Dataset.Builder(); |
| for (Entry<String, AutofillId> field : fields.entrySet()) { |
| String hint = field.getKey(); |
| AutofillId id = field.getValue(); |
| String value = i + "-" + hint; |
| IntentSender authentication = |
| SimpleAuthActivity.newIntentSenderForDataset(context, unlockedDataset); |
| RemoteViews presentation = newDatasetPresentation(packageName, |
| "Tap to auth " + value); |
| lockedDataset.setValue(id, null, presentation) |
| .setAuthentication(authentication); |
| } |
| response.addDataset(lockedDataset.build()); |
| } else { |
| response.addDataset(unlockedDataset); |
| } |
| } |
| |
| // 2.Add save info |
| Collection<AutofillId> ids = fields.values(); |
| AutofillId[] requiredIds = new AutofillId[ids.size()]; |
| ids.toArray(requiredIds); |
| response.setSaveInfo( |
| // We're simple, so we're generic |
| new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build()); |
| |
| // 3.Profit! |
| return response.build(); |
| } |
| |
| static Dataset newUnlockedDataset(@NonNull Map<String, AutofillId> fields, |
| @NonNull String packageName, int i) { |
| Dataset.Builder dataset = new Dataset.Builder(); |
| for (Entry<String, AutofillId> field : fields.entrySet()) { |
| String hint = field.getKey(); |
| AutofillId id = field.getValue(); |
| String value = i + "-" + hint; |
| |
| // We're simple - our dataset values are hardcoded as "N-hint" (for example, |
| // "1-username", "2-username") and they're displayed as such, except if they're a |
| // password |
| String displayValue = hint.contains("password") ? "password for #" + i : value; |
| RemoteViews presentation = newDatasetPresentation(packageName, displayValue); |
| dataset.setValue(id, AutofillValue.forText(value), presentation); |
| } |
| |
| return dataset.build(); |
| } |
| |
| /** |
| * Displays a toast with the given message. |
| */ |
| private void toast(@NonNull CharSequence message) { |
| Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); |
| } |
| } |