Files
android_development/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java
Adam He d60f629802 Migrate to new APIs utilizing
android.widget.inline.InlinePreesntationSpecs.

Bug: 151262653
Test: manual verification
Change-Id: Ia03d463d6e7b18c34de95a9cde639bb307c301ef
2020-03-26 15:49:14 -07:00

363 lines
15 KiB
Java

/*
* Copyright (C) 2019 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 foo.bar.inline;
import android.app.PendingIntent;
import android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.ViewNode;
import android.app.slice.Slice;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.graphics.drawable.Icon;
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.InlinePresentation;
import android.service.autofill.SaveCallback;
import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Size;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.inline.InlinePresentationSpec;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.widget.RemoteViews;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.autofill.InlinePresentationBuilder;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* A basic {@link AutofillService} implementation that only shows dynamic-generated datasets
* and supports inline suggestions.
*/
public class InlineFillService extends AutofillService {
private static final String TAG = "InlineFillService";
/**
* Number of datasets sent on each request - we're simple, that value is hardcoded in our DNA!
*/
static final int NUMBER_DATASETS = 6;
private final boolean mAuthenticateResponses = false;
private final boolean mAuthenticateDatasets = false;
@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, request.getFlags());
Log.d(TAG, "autofillable fields:" + fields);
if (fields.isEmpty()) {
showMessage("Service could not figure out how to autofill this screen");
callback.onSuccess(null);
return;
}
final InlineSuggestionsRequest inlineRequest = request.getInlineSuggestionsRequest();
final int maxSuggestionsCount = inlineRequest == null
? NUMBER_DATASETS
: Math.min(inlineRequest.getMaxSuggestionCount(), NUMBER_DATASETS);
// Create the base response
final 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 = AuthActivity.newIntentSenderForResponse(this, hints,
ids, mAuthenticateDatasets, inlineRequest);
RemoteViews presentation = newDatasetPresentation(getPackageName(),
"Tap to auth response");
final InlinePresentation inlinePresentation;
if (inlineRequest != null) {
final Slice authSlice = new InlinePresentationBuilder("Tap to auth respones")
.build();
final List<InlinePresentationSpec> specs = inlineRequest.getInlinePresentationSpecs();
final int specsSize = specs.size();
final InlinePresentationSpec currentSpec = specsSize > 0 ? specs.get(0) : null;
inlinePresentation = new InlinePresentation(authSlice, currentSpec,
/* pined= */ false);
} else {
inlinePresentation = null;
}
response = new FillResponse.Builder()
.setAuthentication(ids, authentication, presentation, inlinePresentation)
.build();
} else {
response = createResponse(this, fields, maxSuggestionsCount, mAuthenticateDatasets,
request.getInlineSuggestionsRequest());
}
callback.onSuccess(response);
}
static FillResponse createResponse(@NonNull Context context,
@NonNull ArrayMap<String, AutofillId> fields, int numDatasets,
boolean authenticateDatasets, @Nullable InlineSuggestionsRequest inlineRequest) {
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(context, fields, packageName, i,
inlineRequest);
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 =
AuthActivity.newIntentSenderForDataset(context, unlockedDataset);
RemoteViews presentation = newDatasetPresentation(packageName,
"Tap to auth " + value);
final InlinePresentation inlinePresentation;
if (inlineRequest != null) {
final Slice authSlice = new InlinePresentationBuilder(
"Tap to auth " + value).build();
final List<InlinePresentationSpec> specs
= inlineRequest.getInlinePresentationSpecs();
final int specsSize = specs.size();
final InlinePresentationSpec currentSpec =
specsSize > 0 ? specs.get(0) : null;
inlinePresentation = new InlinePresentation(authSlice, currentSpec,
/* pined= */ false);
lockedDataset.setValue(id, null, presentation, inlinePresentation)
.setAuthentication(authentication);
} else {
lockedDataset.setValue(id, null, presentation)
.setAuthentication(authentication);
}
}
response.addDataset(lockedDataset.build());
} else {
response.addDataset(unlockedDataset);
}
}
// if (inlineRequest != null) {
// // Reuse the first spec's height for the inline action size, as there isn't dedicated
// // value from the request for this.
// final int height = inlineRequest.getPresentationSpecs().get(0).getMinSize().getHeight();
// final Size actionIconSize = new Size(height, height);
// response.addDataset(
// newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
// fields));
// }
// 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 newInlineActionDataset(@NonNull Context context,
@NonNull Size size, int drawable, ArrayMap<String, AutofillId> fields) {
Intent intent = new Intent().setComponent(
new ComponentName(context.getPackageName(), SettingsActivity.class.getName()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
final Slice suggestionSlice = new InlinePresentationBuilder()
.setStartIcon(Icon.createWithResource(context, drawable))
.setAttribution(pendingIntent)
.build();
final InlinePresentationSpec currentSpec = new InlinePresentationSpec.Builder(size,
size).build();
Dataset.Builder builder = new Dataset.Builder()
.setInlinePresentation(
new InlinePresentation(suggestionSlice, currentSpec, /** pined= */true))
.setAuthentication(pendingIntent.getIntentSender());
for (AutofillId fieldId : fields.values()) {
builder.setValue(fieldId, null);
}
return builder.build();
}
static Dataset newUnlockedDataset(@NonNull Context context,
@NonNull Map<String, AutofillId> fields, @NonNull String packageName, int i,
@Nullable InlineSuggestionsRequest inlineRequest) {
Dataset.Builder dataset = new Dataset.Builder();
for (Entry<String, AutofillId> field : fields.entrySet()) {
final String hint = field.getKey();
final AutofillId id = field.getValue();
final String value = hint + i;
// We're simple - our dataset values are hardcoded as "hintN" (for example,
// "username1", "username2") and they're displayed as such, except if they're a
// password
final String displayValue = hint.contains("password") ? "password for #" + i : value;
final RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
// Add Inline Suggestion required info.
if (inlineRequest != null) {
Log.d(TAG, "Found InlineSuggestionsRequest in FillRequest: " + inlineRequest);
Intent intent = new Intent().setComponent(
new ComponentName(context.getPackageName(), SettingsActivity.class.getName()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
final Slice suggestionSlice = new InlinePresentationBuilder(value)
.setAttribution(pendingIntent).build();
final List<InlinePresentationSpec> specs = inlineRequest.getInlinePresentationSpecs();
final int specsSize = specs.size();
final InlinePresentationSpec currentSpec = i - 1 < specsSize
? specs.get(i - 1)
: specs.get(specsSize - 1);
final InlinePresentation inlinePresentation =
new InlinePresentation(suggestionSlice, currentSpec, /** pined= */false);
dataset.setValue(id, AutofillValue.forText(value), presentation,
inlinePresentation);
} else {
dataset.setValue(id, AutofillValue.forText(value), presentation);
}
}
return dataset.build();
}
@Override
public void onSaveRequest(SaveRequest request, SaveCallback callback) {
Log.d(TAG, "onSaveRequest()");
showMessage("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 getHint(ViewNode) method.
*/
@NonNull
private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure,
int flags) {
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, flags);
}
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, int flags) {
int type = node.getAutofillType();
String hint = getHint(node, flags);
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), flags);
}
}
/**
* Gets the autofill hint associated with the given node.
*
* <p>By default it just return the first entry on the node's
* {@link ViewNode#getAutofillHints() autofillHints} (when available), but subclasses could
* extend it to use heuristics when the app developer didn't explicitly provide these hints.
*/
@Nullable
protected String getHint(@NonNull ViewNode node, int flags) {
String[] hints = node.getAutofillHints();
if (hints == null) return null;
// We're simple, we only care about the first hint
String hint = hints[0].toLowerCase();
return hint;
}
/**
* Helper method to get the {@link AssistStructure} associated with the latest request
* in an autofill context.
*/
@NonNull
private static AssistStructure getLatestAssistStructure(@NonNull FillRequest request) {
List<FillContext> fillContexts = request.getFillContexts();
return fillContexts.get(fillContexts.size() - 1).getStructure();
}
/**
* Helper method to create a dataset presentation with the given text.
*/
@NonNull
private static RemoteViews newDatasetPresentation(@NonNull String packageName,
@NonNull CharSequence text) {
RemoteViews presentation =
new RemoteViews(packageName, R.layout.list_item);
presentation.setTextViewText(R.id.text, text);
return presentation;
}
/**
* Displays a toast with the given message.
*/
private void showMessage(@NonNull CharSequence message) {
Log.i(TAG, message.toString());
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
}