blob: 501801e87408e803ed21056e32b2cf775deeab9e [file] [log] [blame]
/*
* 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();
}
}