blob: af8a9afa7f7be2b4dc50d84b2c8b6407264f9cca [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 android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.ViewNode;
import android.os.CancellationSignal;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
import android.service.autofill.FillCallback;
import android.service.autofill.FillContext;
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.v4.util.ArrayMap;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.example.android.autofill.service.R;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* A very basic {@link AutofillService} implementation that only shows dynamic-generated datasets
* and don't persist the saved data.
*
* <p>This class has 2 goals:
* <ul>
* <li>Provide a simple service that app developers can use to see how their apps behave with
* autofill.
* <li>Explain the basic autofill workflow / provide a service that can be easily modified.
* </ul>
*/
/*
* TODO list:
* - improve documentation above, explaining
* - no-goals, security limitations, etc..
* - toast usage
* - how dynamic datasets are created
* - how save works
* - use strings instead of hardcoded values
* - use different icons for different services
*/
public class BasicService extends AutofillService {
private static final String TAG = "BasicService";
private static final int NUMBER_DATASETS = 4;
@Override
public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
FillCallback callback) {
Log.d(TAG, "onFillRequest()");
// Find autofillable fields
AssistStructure structure = getLatestAssistStructure(request);
Map<String, AutofillId> fields = getAutofillableFields(structure);
Log.d(TAG, "autofillable fields:" + fields);
if (fields.isEmpty()) {
toast("No autofill hints found");
callback.onSuccess(null);
return;
}
Log.d(TAG, "autofill hints: " + fields);
// Create the base response
FillResponse.Builder response = new FillResponse.Builder();
// 1.Add the dynamic datasets
String packageName = getApplicationContext().getPackageName();
for (int i = 1; i <= NUMBER_DATASETS; i++) {
Dataset.Builder dataset = new Dataset.Builder();
for (Entry<String, AutofillId> field : fields.entrySet()) {
String hint = field.getKey();
AutofillId id = field.getValue();
String value = hint + i;
String displayValue = hint.contains("password") ? "password for #" + i : value;
RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
dataset.setValue(id, AutofillValue.forText(value), presentation);
}
response.addDataset(dataset.build());
}
// 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!
callback.onSuccess(response.build());
}
@Override
public void onSaveRequest(SaveRequest request, SaveCallback callback) {
Log.d(TAG, "onSaveRequest()");
toast("Save not supported");
callback.onSuccess();
}
// TODO: document
@NonNull
private Map<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) {
Map<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;
}
// TODO: document
private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
@NonNull ViewNode node) {
String[] hints = node.getAutofillHints();
if (hints != null) {
AutofillId id = node.getAutofillId();
// We're simple, we only care about the first hint
String hint = hints[0].toLowerCase();
Log.v(TAG, "Found hint " + hint + " on " + id);
fields.put(hint, id);
}
int childrenSize = node.getChildCount();
for (int i = 0; i < childrenSize; i++) {
addAutofillableFields(fields, node.getChildAt(i));
}
}
// TODO: move to common code
@NonNull
private static AssistStructure getLatestAssistStructure(@NonNull FillRequest request) {
List<FillContext> fillContexts = request.getFillContexts();
return fillContexts.get(fillContexts.size() - 1).getStructure();
}
// TODO: move to common code
@NonNull
private static RemoteViews newDatasetPresentation(@NonNull String packageName,
@NonNull CharSequence text) {
RemoteViews presentation =
new RemoteViews(packageName, R.layout.multidataset_service_list_item);
presentation.setTextViewText(R.id.text, text);
presentation.setImageViewResource(R.id.icon, R.mipmap.ic_launcher);
return presentation;
}
// TODO: move to common code
private void toast(@NonNull CharSequence message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
}