Response and Dataset authentication via inline autofill.

Bug: 148815880
Test: manual verification
Change-Id: Ic72f6d828521737db5ce890c0e79cd6f8d16a6c6
This commit is contained in:
Adam He
2020-02-03 15:53:32 -08:00
parent 677db1865c
commit 5be0f15801
8 changed files with 169 additions and 42 deletions

View File

@@ -43048,6 +43048,7 @@ package android.service.autofill {
method @NonNull public android.service.autofill.FillResponse build();
method @NonNull public android.service.autofill.FillResponse.Builder disableAutofill(long);
method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews);
method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation);
method @NonNull public android.service.autofill.FillResponse.Builder setClientState(@Nullable android.os.Bundle);
method @NonNull public android.service.autofill.FillResponse.Builder setFieldClassificationIds(@NonNull android.view.autofill.AutofillId...);
method @NonNull public android.service.autofill.FillResponse.Builder setFlags(int);

View File

@@ -1907,6 +1907,7 @@ public class Activity extends ContextThemeWrapper
if (!mAutoFillIgnoreFirstResumePause) {
View focus = getCurrentFocus();
if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
// TODO(b/148815880): Bring up keyboard if resumed from inline authentication.
// TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest#
// testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial
// window visibility after recreation is INVISIBLE in onResume() and next frame
@@ -8467,7 +8468,7 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
public final void autofillClientAuthenticate(int authenticationId, IntentSender intent,
Intent fillInIntent) {
Intent fillInIntent, boolean authenticateInline) {
try {
startIntentSenderForResultInner(intent, AUTO_FILL_AUTH_WHO_PREFIX,
authenticationId, fillInIntent, 0, 0, null);

View File

@@ -75,6 +75,7 @@ public final class FillResponse implements Parcelable {
private final @Nullable SaveInfo mSaveInfo;
private final @Nullable Bundle mClientState;
private final @Nullable RemoteViews mPresentation;
private final @Nullable InlinePresentation mInlinePresentation;
private final @Nullable RemoteViews mHeader;
private final @Nullable RemoteViews mFooter;
private final @Nullable IntentSender mAuthentication;
@@ -95,6 +96,7 @@ public final class FillResponse implements Parcelable {
mSaveInfo = builder.mSaveInfo;
mClientState = builder.mClientState;
mPresentation = builder.mPresentation;
mInlinePresentation = builder.mInlinePresentation;
mHeader = builder.mHeader;
mFooter = builder.mFooter;
mAuthentication = builder.mAuthentication;
@@ -130,6 +132,11 @@ public final class FillResponse implements Parcelable {
return mPresentation;
}
/** @hide */
public @Nullable InlinePresentation getInlinePresentation() {
return mInlinePresentation;
}
/** @hide */
public @Nullable RemoteViews getHeader() {
return mHeader;
@@ -219,6 +226,7 @@ public final class FillResponse implements Parcelable {
private SaveInfo mSaveInfo;
private Bundle mClientState;
private RemoteViews mPresentation;
private InlinePresentation mInlinePresentation;
private RemoteViews mHeader;
private RemoteViews mFooter;
private IntentSender mAuthentication;
@@ -317,6 +325,67 @@ public final class FillResponse implements Parcelable {
return this;
}
/**
* Triggers a custom UI before before autofilling the screen with any data set in this
* response.
*
* <p><b>Note:</b> Although the name of this method suggests that it should be used just for
* authentication flow, it can be used for other advanced flows; see {@link AutofillService}
* for examples.
*
* <p>This method is similar to
* {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, but also accepts
* an {@link InlinePresentation} presentation which is required for authenticating through
* the inline autofill flow.
*
* <p><b>Note:</b> {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does
* not work with {@link InlinePresentation}.</p>
*
* @param authentication Intent to an activity with your authentication flow.
* @param presentation The presentation to visualize the response.
* @param inlinePresentation The inlinePresentation to visualize the response inline.
* @param ids id of Views that when focused will display the authentication UI.
*
* @return This builder.
*
* @throws IllegalArgumentException if any of the following occurs:
* <ul>
* <li>{@code ids} is {@code null}</li>
* <li>{@code ids} is empty</li>
* <li>{@code ids} contains a {@code null} element</li>
* <li>both {@code authentication} and {@code presentation} are {@code null}</li>
* <li>both {@code authentication} and {@code presentation} are non-{@code null}</li>
* <li>both {@code authentication} and {@code inlinePresentation} are {@code null}</li>
* <li>both {@code authentication} and {@code inlinePresentation} are
* non-{@code null}</li>
* </ul>
*
* @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a
* {@link #setFooter(RemoteViews) footer} are already set for this builder.
*
* @see android.app.PendingIntent#getIntentSender()
*/
@NonNull
public Builder setAuthentication(@NonNull AutofillId[] ids,
@Nullable IntentSender authentication, @Nullable RemoteViews presentation,
@Nullable InlinePresentation inlinePresentation) {
throwIfDestroyed();
throwIfDisableAutofillCalled();
if (mHeader != null || mFooter != null) {
throw new IllegalStateException("Already called #setHeader() or #setFooter()");
}
if (authentication == null ^ (presentation == null && inlinePresentation == null)) {
throw new IllegalArgumentException("authentication and presentation "
+ "(dropdown or inline), must be both non-null or null");
}
mAuthentication = authentication;
mPresentation = presentation;
mInlinePresentation = inlinePresentation;
mAuthenticationIds = assertValid(ids);
return this;
}
/**
* Specifies views that should not trigger new
* {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
@@ -644,6 +713,8 @@ public final class FillResponse implements Parcelable {
break;
}
}
} else if (mInlinePresentation != null) {
mSupportsInlineSuggestions = true;
}
mDestroyed = true;
@@ -691,6 +762,9 @@ public final class FillResponse implements Parcelable {
if (mPresentation != null) {
builder.append(", hasPresentation");
}
if (mInlinePresentation != null) {
builder.append(", hasInlinePresentation");
}
if (mHeader != null) {
builder.append(", hasHeader");
}
@@ -740,6 +814,7 @@ public final class FillResponse implements Parcelable {
parcel.writeParcelableArray(mAuthenticationIds, flags);
parcel.writeParcelable(mAuthentication, flags);
parcel.writeParcelable(mPresentation, flags);
parcel.writeParcelable(mInlinePresentation, flags);
parcel.writeParcelable(mHeader, flags);
parcel.writeParcelable(mFooter, flags);
parcel.writeParcelable(mUserData, flags);
@@ -774,8 +849,10 @@ public final class FillResponse implements Parcelable {
AutofillId.class);
final IntentSender authentication = parcel.readParcelable(null);
final RemoteViews presentation = parcel.readParcelable(null);
final InlinePresentation inlinePresentation = parcel.readParcelable(null);
if (authenticationIds != null) {
builder.setAuthentication(authenticationIds, authentication, presentation);
builder.setAuthentication(authenticationIds, authentication, presentation,
inlinePresentation);
}
final RemoteViews header = parcel.readParcelable(null);
if (header != null) {

View File

@@ -86,9 +86,10 @@ import java.util.List;
import java.util.Objects;
import java.util.Set;
//TODO: use java.lang.ref.Cleaner once Android supports Java 9
import sun.misc.Cleaner;
//TODO: use java.lang.ref.Cleaner once Android supports Java 9
/**
* <p>The {@link AutofillManager} class provides ways for apps and custom views to
* integrate with the Autofill Framework lifecycle.
@@ -547,7 +548,7 @@ public final class AutofillManager {
* @param fillInIntent The authentication fill-in intent.
*/
void autofillClientAuthenticate(int authenticationId, IntentSender intent,
Intent fillInIntent);
Intent fillInIntent, boolean authenticateInline);
/**
* Tells the client this manager has state to be reset.
@@ -2070,7 +2071,7 @@ public final class AutofillManager {
}
private void authenticate(int sessionId, int authenticationId, IntentSender intent,
Intent fillInIntent) {
Intent fillInIntent, boolean authenticateInline) {
synchronized (mLock) {
if (sessionId == mSessionId) {
final AutofillClient client = getClient();
@@ -2078,7 +2079,8 @@ public final class AutofillManager {
// clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill()
// before onAuthenticationResult()
mOnInvisibleCalled = false;
client.autofillClientAuthenticate(authenticationId, intent, fillInIntent);
client.autofillClientAuthenticate(authenticationId, intent, fillInIntent,
authenticateInline);
}
}
}
@@ -3250,10 +3252,11 @@ public final class AutofillManager {
@Override
public void authenticate(int sessionId, int authenticationId, IntentSender intent,
Intent fillInIntent) {
Intent fillInIntent, boolean authenticateInline) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.post(() -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent));
afm.post(() -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent,
authenticateInline));
}
}

View File

@@ -50,7 +50,7 @@ oneway interface IAutoFillManagerClient {
* Authenticates a fill response or a data set.
*/
void authenticate(int sessionId, int authenticationId, in IntentSender intent,
in Intent fillInIntent);
in Intent fillInIntent, boolean authenticateInline);
/**
* Sets the views to track. If saveOnAllViewsInvisible is set and all these view are invisible

View File

@@ -1018,7 +1018,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// FillServiceCallbacks
@Override
public void authenticate(int requestId, int datasetIndex, IntentSender intent, Bundle extras) {
public void authenticate(int requestId, int datasetIndex, IntentSender intent, Bundle extras,
boolean authenticateInline) {
if (sDebug) {
Slog.d(TAG, "authenticate(): requestId=" + requestId + "; datasetIdx=" + datasetIndex
+ "; intentSender=" + intent);
@@ -1042,7 +1043,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex);
mHandler.sendMessage(obtainMessage(
Session::startAuthentication,
this, authenticationId, intent, fillInIntent));
this, authenticationId, intent, fillInIntent, authenticateInline));
}
// VultureCallback
@@ -2623,8 +2624,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mService.getServicePackageName(), mComponentName,
serviceLabel, serviceIcon, this, id, mCompatMode);
mService.logDatasetShown(id, mClientState);
synchronized (mLock) {
if (mUiShownTime == 0) {
// Log first time UI is shown.
@@ -2656,10 +2655,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response,
@Nullable String filterText) {
final List<Dataset> datasets = response.getDatasets();
if (datasets == null) {
Log.w(TAG, "response returned null datasets");
return false;
}
final InlineSuggestionSession.ImeResponse imeResponse =
mInlineSuggestionSession.waitAndGetImeResponse();
@@ -2671,9 +2666,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final InlineSuggestionsRequest request = imeResponse.getRequest();
InlineSuggestionsResponse inlineSuggestionsResponse =
InlineSuggestionFactory.createInlineSuggestionsResponse(request,
response.getRequestId(),
datasets.toArray(new Dataset[]{}), filterText, response.getInlineActions(),
mCurrentViewId, mContext, this, () -> {
response, filterText, response.getInlineActions(), mCurrentViewId, mContext,
this, () -> {
synchronized (mLock) {
requestHideFillUi(mCurrentViewId);
}
@@ -3160,7 +3154,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
final int authenticationId = AutofillManager.makeAuthenticationId(requestId,
datasetIndex);
startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent);
startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent,
/* authenticateInline= */false);
}
}
@@ -3184,10 +3179,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
private void startAuthentication(int authenticationId, IntentSender intent,
Intent fillInIntent) {
Intent fillInIntent, boolean authenticateInline) {
try {
synchronized (mLock) {
mClient.authenticate(id, authenticationId, intent, fillInIntent);
mClient.authenticate(id, authenticationId, intent, fillInIntent,
authenticateInline);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);

View File

@@ -79,7 +79,7 @@ public final class AutoFillUI {
public interface AutoFillUiCallback {
void authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent,
@Nullable Bundle extras);
@Nullable Bundle extras, boolean authenticateInline);
void fill(int requestId, int datasetIndex, @NonNull Dataset dataset);
void save();
void cancelSave();
@@ -217,7 +217,8 @@ public final class AutoFillUI {
if (mCallback != null) {
mCallback.authenticate(response.getRequestId(),
AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED,
response.getAuthentication(), response.getClientState());
response.getAuthentication(), response.getClientState(),
/* authenticateInline= */ false);
}
}

View File

@@ -25,12 +25,14 @@ import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import android.service.autofill.Dataset;
import android.service.autofill.FillResponse;
import android.service.autofill.IInlineSuggestionUiCallback;
import android.service.autofill.InlinePresentation;
import android.text.TextUtils;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.inline.InlinePresentationSpec;
import android.view.inputmethod.InlineSuggestion;
@@ -69,8 +71,7 @@ public final class InlineSuggestionFactory {
* autofill service, potentially filtering the datasets.
*/
public static InlineSuggestionsResponse createInlineSuggestionsResponse(
@NonNull InlineSuggestionsRequest request, int requestId,
@NonNull Dataset[] datasets,
@NonNull InlineSuggestionsRequest request, @NonNull FillResponse response,
@Nullable String filterText,
@Nullable List<InlinePresentation> inlineActions,
@NonNull AutofillId autofillId,
@@ -79,10 +80,24 @@ public final class InlineSuggestionFactory {
@NonNull Runnable onErrorCallback,
@Nullable RemoteInlineSuggestionRenderService remoteRenderService) {
if (sDebug) Slog.d(TAG, "createInlineSuggestionsResponse called");
return createInlineSuggestionsResponseInternal(/* isAugmented= */ false, request, datasets,
filterText, inlineActions, autofillId, context, onErrorCallback,
(dataset, datasetIndex) -> client.fill(requestId, datasetIndex, dataset),
remoteRenderService);
final BiConsumer<Dataset, Integer> onClickFactory;
if (response.getAuthentication() != null) {
onClickFactory = (dataset, datasetIndex) -> client.authenticate(response.getRequestId(),
datasetIndex, response.getAuthentication(), response.getClientState(),
/* authenticateInline= */ true);
} else {
onClickFactory = (dataset, datasetIndex) ->
client.fill(response.getRequestId(), datasetIndex, dataset);
}
final List<Dataset> datasetList = response.getDatasets();
final Dataset[] datasets = datasetList == null
? null
: datasetList.toArray(new Dataset[]{});
return createInlineSuggestionsResponseInternal(/* isAugmented= */ false, request, response,
datasets, filterText, inlineActions, autofillId, context, onErrorCallback,
onClickFactory, remoteRenderService);
}
/**
@@ -99,22 +114,38 @@ public final class InlineSuggestionFactory {
@Nullable RemoteInlineSuggestionRenderService remoteRenderService) {
if (sDebug) Slog.d(TAG, "createAugmentedInlineSuggestionsResponse called");
return createInlineSuggestionsResponseInternal(/* isAugmented= */ true, request,
datasets, /* filterText= */ null, /* inlineActions= */ null, autofillId, context,
onErrorCallback,
(dataset, fieldIndex) -> inlineSuggestionUiCallback.autofill(dataset),
remoteRenderService);
/* fillResponse= */ null, datasets, /* filterText= */ null,
/* inlineActions= */ null, autofillId, context, onErrorCallback,
(dataset, fieldIndex) ->
inlineSuggestionUiCallback.autofill(dataset), remoteRenderService);
}
private static InlineSuggestionsResponse createInlineSuggestionsResponseInternal(
boolean isAugmented, @NonNull InlineSuggestionsRequest request,
@NonNull Dataset[] datasets, @Nullable String filterText,
@Nullable FillResponse response, @Nullable Dataset[] datasets,
@Nullable String filterText,
@Nullable List<InlinePresentation> inlineActions, @NonNull AutofillId autofillId,
@NonNull Context context, @NonNull Runnable onErrorCallback,
@NonNull BiConsumer<Dataset, Integer> onClickFactory,
@Nullable RemoteInlineSuggestionRenderService remoteRenderService) {
final ArrayList<InlineSuggestion> inlineSuggestions = new ArrayList<>();
for (int i = 0; i < datasets.length; i++) {
final Dataset dataset = datasets[i];
if (response.getAuthentication() != null) {
InlineSuggestion inlineAuthSuggestion = createInlineAuthSuggestion(response,
remoteRenderService, onClickFactory, onErrorCallback,
request.getHostInputToken());
inlineSuggestions.add(inlineAuthSuggestion);
return new InlineSuggestionsResponse(inlineSuggestions);
}
if (datasets == null) {
Slog.w(TAG, "Datasets should not be null here");
return null;
}
for (int datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) {
final Dataset dataset = datasets[datasetIndex];
final int fieldIndex = dataset.getFieldIds().indexOf(autofillId);
if (fieldIndex < 0) {
Slog.w(TAG, "AutofillId=" + autofillId + " not found in dataset");
@@ -130,9 +161,10 @@ public final class InlineSuggestionFactory {
continue;
}
InlineSuggestion inlineSuggestion = createInlineSuggestion(isAugmented, dataset,
fieldIndex, mergedInlinePresentation(request, i, inlinePresentation),
fieldIndex, mergedInlinePresentation(request, datasetIndex, inlinePresentation),
onClickFactory, remoteRenderService, onErrorCallback,
request.getHostInputToken());
inlineSuggestions.add(inlineSuggestion);
}
if (inlineActions != null) {
@@ -198,7 +230,7 @@ public final class InlineSuggestionFactory {
}
private static InlineSuggestion createInlineSuggestion(boolean isAugmented,
@NonNull Dataset dataset, int fieldIndex,
@NonNull Dataset dataset, int datasetIndex,
@NonNull InlinePresentation inlinePresentation,
@NonNull BiConsumer<Dataset, Integer> onClickFactory,
@NonNull RemoteInlineSuggestionRenderService remoteRenderService,
@@ -212,12 +244,28 @@ public final class InlineSuggestionFactory {
final InlineSuggestion inlineSuggestion = new InlineSuggestion(inlineSuggestionInfo,
createInlineContentProvider(inlinePresentation,
() -> onClickFactory.accept(dataset, fieldIndex), onErrorCallback,
() -> onClickFactory.accept(dataset, datasetIndex), onErrorCallback,
remoteRenderService, hostInputToken));
return inlineSuggestion;
}
private static InlineSuggestion createInlineAuthSuggestion(@NonNull FillResponse response,
@NonNull RemoteInlineSuggestionRenderService remoteRenderService,
@NonNull BiConsumer<Dataset, Integer> onClickFactory, @NonNull Runnable onErrorCallback,
@Nullable IBinder hostInputToken) {
final InlinePresentation inlinePresentation = response.getInlinePresentation();
final InlineSuggestionInfo inlineSuggestionInfo = new InlineSuggestionInfo(
inlinePresentation.getInlinePresentationSpec(),
InlineSuggestionInfo.SOURCE_AUTOFILL, null, InlineSuggestionInfo.TYPE_SUGGESTION);
return new InlineSuggestion(inlineSuggestionInfo,
createInlineContentProvider(inlinePresentation,
() -> onClickFactory.accept(null,
AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED),
onErrorCallback, remoteRenderService, hostInputToken));
}
/**
* Returns an {@link InlinePresentation} with the style spec from the request/host, and
* everything else from the provided {@code inlinePresentation}.
@@ -299,4 +347,4 @@ public final class InlineSuggestionFactory {
private InlineSuggestionFactory() {
}
}
}