summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt6
-rw-r--r--core/java/android/service/autofill/FillRequest.java2
-rw-r--r--core/java/android/view/autofill/AutofillManager.java92
-rw-r--r--core/java/android/view/autofill/AutofillRequestCallback.java72
-rw-r--r--core/java/android/view/autofill/IAutoFillManagerClient.aidl8
-rw-r--r--services/autofill/java/com/android/server/autofill/ClientSuggestionsSession.java293
-rw-r--r--services/autofill/java/com/android/server/autofill/Session.java120
7 files changed, 566 insertions, 27 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index e74d1f3b9ef6..e9281fde480c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -50901,6 +50901,7 @@ package android.view.autofill {
public final class AutofillManager {
method public void cancel();
+ method public void clearAutofillRequestCallback();
method public void commit();
method public void disableAutofillServices();
method @Nullable public android.content.ComponentName getAutofillServiceComponentName();
@@ -50926,6 +50927,7 @@ package android.view.autofill {
method public void registerCallback(@Nullable android.view.autofill.AutofillManager.AutofillCallback);
method public void requestAutofill(@NonNull android.view.View);
method public void requestAutofill(@NonNull android.view.View, int, @NonNull android.graphics.Rect);
+ method public void setAutofillRequestCallback(@NonNull java.util.concurrent.Executor, @NonNull android.view.autofill.AutofillRequestCallback);
method public void setUserData(@Nullable android.service.autofill.UserData);
method public void unregisterCallback(@Nullable android.view.autofill.AutofillManager.AutofillCallback);
field public static final String EXTRA_ASSIST_STRUCTURE = "android.view.autofill.extra.ASSIST_STRUCTURE";
@@ -50944,6 +50946,10 @@ package android.view.autofill {
field public static final int EVENT_INPUT_UNAVAILABLE = 3; // 0x3
}
+ public interface AutofillRequestCallback {
+ method public void onFillRequest(@Nullable android.view.inputmethod.InlineSuggestionsRequest, @NonNull android.os.CancellationSignal, @NonNull android.service.autofill.FillCallback);
+ }
+
public final class AutofillValue implements android.os.Parcelable {
method public int describeContents();
method public static android.view.autofill.AutofillValue forDate(long);
diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java
index 62becc507404..af846b62ae2c 100644
--- a/core/java/android/service/autofill/FillRequest.java
+++ b/core/java/android/service/autofill/FillRequest.java
@@ -96,6 +96,8 @@ public final class FillRequest implements Parcelable {
*/
public static final @RequestFlags int FLAG_VIEW_NOT_FOCUSED = 0x10;
+ // The flag value 0x20 has been defined in AutofillManager.
+
/** @hide */
public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE;
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 208cd967dc99..b0b9d244f0d8 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -25,6 +25,7 @@ import static android.view.autofill.Helper.sVerbose;
import static android.view.autofill.Helper.toList;
import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -45,16 +46,21 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.metrics.LogMaker;
+import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.CancellationSignal;
import android.os.Handler;
import android.os.IBinder;
+import android.os.ICancellationSignal;
import android.os.Looper;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
import android.service.autofill.AutofillService;
+import android.service.autofill.FillCallback;
import android.service.autofill.FillEventHistory;
+import android.service.autofill.IFillCallback;
import android.service.autofill.UserData;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -74,6 +80,7 @@ import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
@@ -99,6 +106,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executor;
import sun.misc.Cleaner;
@@ -167,6 +175,12 @@ import sun.misc.Cleaner;
* shows an autofill save UI if the value of savable views have changed. If the user selects the
* option to Save, the current value of the views is then sent to the autofill service.
*
+ * <p>There is another choice for the application to provide it's datasets to the Autofill framework
+ * by setting an {@link AutofillRequestCallback} through
+ * {@link #setAutofillRequestCallback(Executor, AutofillRequestCallback)}. The application can use
+ * its callback instead of the default {@link AutofillService}. See
+ * {@link AutofillRequestCallback} for more details.
+ *
* <h3 id="additional-notes">Additional notes</h3>
*
* <p>It is safe to call <code>AutofillManager</code> methods from any thread.
@@ -292,6 +306,7 @@ public final class AutofillManager {
/** @hide */ public static final int FLAG_ADD_CLIENT_DEBUG = 0x2;
/** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4;
/** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY = 0x8;
+ /** @hide */ public static final int FLAG_ENABLED_CLIENT_SUGGESTIONS = 0x20;
// NOTE: flag below is used by the session start receiver only, hence it can have values above
/** @hide */ public static final int RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY = 0x1;
@@ -592,6 +607,11 @@ public final class AutofillManager {
@GuardedBy("mLock")
private boolean mEnabledForAugmentedAutofillOnly;
+ @GuardedBy("mLock")
+ @Nullable private AutofillRequestCallback mAutofillRequestCallback;
+ @GuardedBy("mLock")
+ @Nullable private Executor mRequestCallbackExecutor;
+
/** @hide */
public interface AutofillClient {
/**
@@ -1836,6 +1856,32 @@ public final class AutofillManager {
return new AutofillId(parent.getAutofillViewId(), virtualId);
}
+ /**
+ * Sets the client's suggestions callback for autofill.
+ *
+ * @see AutofillRequestCallback
+ *
+ * @param executor specifies the thread upon which the callbacks will be invoked.
+ * @param callback which handles autofill request to provide client's suggestions.
+ */
+ public void setAutofillRequestCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull AutofillRequestCallback callback) {
+ synchronized (mLock) {
+ mRequestCallbackExecutor = executor;
+ mAutofillRequestCallback = callback;
+ }
+ }
+
+ /**
+ * clears the client's suggestions callback for autofill.
+ */
+ public void clearAutofillRequestCallback() {
+ synchronized (mLock) {
+ mRequestCallbackExecutor = null;
+ mAutofillRequestCallback = null;
+ }
+ }
+
@GuardedBy("mLock")
private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds,
@NonNull AutofillValue value, int flags) {
@@ -1896,6 +1942,13 @@ public final class AutofillManager {
}
}
+ if (mAutofillRequestCallback != null) {
+ if (sDebug) {
+ Log.d(TAG, "startSession with the client suggestions provider");
+ }
+ flags |= FLAG_ENABLED_CLIENT_SUGGESTIONS;
+ }
+
mService.startSession(client.autofillClientGetActivityToken(),
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
mCallback != null, flags, componentName,
@@ -2245,6 +2298,28 @@ public final class AutofillManager {
}
}
+ private void onFillRequest(InlineSuggestionsRequest request,
+ CancellationSignal cancellationSignal, FillCallback callback) {
+ final AutofillRequestCallback autofillRequestCallback;
+ final Executor executor;
+ synchronized (mLock) {
+ autofillRequestCallback = mAutofillRequestCallback;
+ executor = mRequestCallbackExecutor;
+ }
+ if (autofillRequestCallback != null && executor != null) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() ->
+ autofillRequestCallback.onFillRequest(
+ request, cancellationSignal, callback));
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ } else {
+ callback.onSuccess(null);
+ }
+ }
+
/** @hide */
public static final int SET_STATE_FLAG_ENABLED = 0x01;
/** @hide */
@@ -3624,6 +3699,23 @@ public final class AutofillManager {
afm.post(() -> afm.requestShowSoftInput(id));
}
}
+
+ @Override
+ public void requestFillFromClient(int id, InlineSuggestionsRequest request,
+ IFillCallback callback) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ ICancellationSignal transport = CancellationSignal.createTransport();
+ try {
+ callback.onCancellable(transport);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error requesting a cancellation", e);
+ }
+
+ afm.onFillRequest(request, CancellationSignal.fromTransport(transport),
+ new FillCallback(callback, id));
+ }
+ }
}
private static final class AugmentedAutofillManagerClient
diff --git a/core/java/android/view/autofill/AutofillRequestCallback.java b/core/java/android/view/autofill/AutofillRequestCallback.java
new file mode 100644
index 000000000000..e632a5849471
--- /dev/null
+++ b/core/java/android/view/autofill/AutofillRequestCallback.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 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 android.view.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.CancellationSignal;
+import android.service.autofill.FillCallback;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+/**
+ * <p>This class is used to provide some input suggestions to the Autofill framework.
+ *
+ * <P>When the user is requested to input something, Autofill will try to query input suggestions
+ * for the user choosing. If the application want to provide some internal input suggestions,
+ * implements this callback and register via
+ * {@link AutofillManager#setAutofillRequestCallback(java.util.concurrent.Executor,
+ * AutofillRequestCallback)}. Autofill will callback the
+ * {@link #onFillRequest(InlineSuggestionsRequest, CancellationSignal, FillCallback)} to request
+ * input suggestions.
+ *
+ * <P>To make sure the callback to take effect, must register before the autofill session starts.
+ * If the autofill session is started, calls {@link AutofillManager#cancel()} to finish current
+ * session, and then the callback will be used at the next restarted session.
+ *
+ * <P>To create a {@link android.service.autofill.FillResponse}, application should fetch
+ * {@link AutofillId}s from its view structure. Below is an example:
+ * <pre class="prettyprint">
+ * AutofillId usernameId = findViewById(R.id.username).getAutofillId();
+ * AutofillId passwordId = findViewById(R.id.password).getAutofillId();
+ * </pre>
+ * To learn more about creating a {@link android.service.autofill.FillResponse}, read
+ * <a href="/guide/topics/text/autofill-services#fill">Fill out client views</a>.
+ *
+ * <P>To fallback to the default {@link android.service.autofill.AutofillService}, just respond
+ * a null of the {@link android.service.autofill.FillResponse}. And then Autofill will do a fill
+ * request with the default {@link android.service.autofill.AutofillService}. Or clear the callback
+ * from {@link AutofillManager} via {@link AutofillManager#clearAutofillRequestCallback()}. If the
+ * client would like to keep no suggestions for the field, respond with an empty
+ * {@link android.service.autofill.FillResponse} which has no dataset.
+ *
+ * <P>IMPORTANT: This should not be used for displaying anything other than input suggestions, or
+ * the keyboard may choose to block your app from the inline strip.
+ */
+public interface AutofillRequestCallback {
+ /**
+ * Called by the Android system to decide if a screen can be autofilled by the callback.
+ *
+ * @param inlineSuggestionsRequest the {@link InlineSuggestionsRequest request} to handle if
+ * currently inline suggestions are supported and can be displayed.
+ * @param cancellationSignal signal for observing cancellation requests. The system will use
+ * this to notify you that the fill result is no longer needed and you should stop
+ * handling this fill request in order to save resources.
+ * @param callback object used to notify the result of the request.
+ */
+ void onFillRequest(@Nullable InlineSuggestionsRequest inlineSuggestionsRequest,
+ @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback);
+}
diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
index 1f833f66c257..64507aac54cb 100644
--- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl
+++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
@@ -24,9 +24,11 @@ import android.content.Intent;
import android.content.IntentSender;
import android.graphics.Rect;
import android.os.IBinder;
+import android.service.autofill.IFillCallback;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.view.autofill.IAutofillWindowPresenter;
+import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.KeyEvent;
import com.android.internal.os.IResultReceiver;
@@ -140,4 +142,10 @@ oneway interface IAutoFillManagerClient {
* Requests to show the soft input method if the focus is on the given id.
*/
void requestShowSoftInput(in AutofillId id);
+
+ /**
+ * Requests to determine if a screen can be autofilled by the client app.
+ */
+ void requestFillFromClient(int id, in InlineSuggestionsRequest request,
+ in IFillCallback callback);
}
diff --git a/services/autofill/java/com/android/server/autofill/ClientSuggestionsSession.java b/services/autofill/java/com/android/server/autofill/ClientSuggestionsSession.java
new file mode 100644
index 000000000000..715697d82cad
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/ClientSuggestionsSession.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2021 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.android.server.autofill;
+
+import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
+
+import static com.android.server.autofill.Helper.sVerbose;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.ICancellationSignal;
+import android.os.RemoteException;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
+import android.service.autofill.IFillCallback;
+import android.service.autofill.SaveInfo;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.Slog;
+import android.view.autofill.AutofillId;
+import android.view.autofill.IAutoFillManagerClient;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.infra.AndroidFuture;
+
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Maintains a client suggestions session with the
+ * {@link android.view.autofill.AutofillRequestCallback} through the {@link IAutoFillManagerClient}.
+ *
+ */
+final class ClientSuggestionsSession {
+
+ private static final String TAG = "ClientSuggestionsSession";
+ private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 15 * DateUtils.SECOND_IN_MILLIS;
+
+ private final int mSessionId;
+ private final IAutoFillManagerClient mClient;
+ private final Handler mHandler;
+ private final ComponentName mComponentName;
+
+ private final RemoteFillService.FillServiceCallbacks mCallbacks;
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private AndroidFuture<FillResponse> mPendingFillRequest;
+ @GuardedBy("mLock")
+ private int mPendingFillRequestId = INVALID_REQUEST_ID;
+
+ ClientSuggestionsSession(int sessionId, IAutoFillManagerClient client, Handler handler,
+ ComponentName componentName, RemoteFillService.FillServiceCallbacks callbacks) {
+ mSessionId = sessionId;
+ mClient = client;
+ mHandler = handler;
+ mComponentName = componentName;
+ mCallbacks = callbacks;
+ }
+
+ void onFillRequest(int requestId, InlineSuggestionsRequest inlineRequest, int flags) {
+ final AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
+ final AtomicReference<AndroidFuture<FillResponse>> futureRef = new AtomicReference<>();
+ final AndroidFuture<FillResponse> fillRequest = new AndroidFuture<>();
+
+ mHandler.post(() -> {
+ if (sVerbose) {
+ Slog.v(TAG, "calling onFillRequest() for id=" + requestId);
+ }
+
+ try {
+ mClient.requestFillFromClient(requestId, inlineRequest,
+ new FillCallbackImpl(fillRequest, futureRef, cancellationSink));
+ } catch (RemoteException e) {
+ fillRequest.completeExceptionally(e);
+ }
+ });
+
+ fillRequest.orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
+ futureRef.set(fillRequest);
+
+ synchronized (mLock) {
+ mPendingFillRequest = fillRequest;
+ mPendingFillRequestId = requestId;
+ }
+
+ fillRequest.whenComplete((res, err) -> mHandler.post(() -> {
+ synchronized (mLock) {
+ mPendingFillRequest = null;
+ mPendingFillRequestId = INVALID_REQUEST_ID;
+ }
+ if (err == null) {
+ processAutofillId(res);
+ mCallbacks.onFillRequestSuccess(requestId, res,
+ mComponentName.getPackageName(), flags);
+ } else {
+ Slog.e(TAG, "Error calling on client fill request", err);
+ if (err instanceof TimeoutException) {
+ dispatchCancellationSignal(cancellationSink.get());
+ mCallbacks.onFillRequestTimeout(requestId);
+ } else if (err instanceof CancellationException) {
+ dispatchCancellationSignal(cancellationSink.get());
+ } else {
+ mCallbacks.onFillRequestFailure(requestId, err.getMessage());
+ }
+ }
+ }));
+ }
+
+ /**
+ * Gets the application info for the component.
+ */
+ @Nullable
+ static ApplicationInfo getAppInfo(ComponentName comp, @UserIdInt int userId) {
+ try {
+ ApplicationInfo si = AppGlobals.getPackageManager().getApplicationInfo(
+ comp.getPackageName(),
+ PackageManager.GET_META_DATA,
+ userId);
+ if (si != null) {
+ return si;
+ }
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ /**
+ * Gets the user-visible name of the application.
+ */
+ @Nullable
+ @GuardedBy("mLock")
+ static CharSequence getAppLabelLocked(Context context, ApplicationInfo appInfo) {
+ return appInfo == null ? null : appInfo.loadSafeLabel(
+ context.getPackageManager(), 0 /* do not ellipsize */,
+ TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM);
+ }
+
+ /**
+ * Gets the user-visible icon of the application.
+ */
+ @Nullable
+ @GuardedBy("mLock")
+ static Drawable getAppIconLocked(Context context, ApplicationInfo appInfo) {
+ return appInfo == null ? null : appInfo.loadIcon(context.getPackageManager());
+ }
+
+ int cancelCurrentRequest() {
+ synchronized (mLock) {
+ return mPendingFillRequest != null && mPendingFillRequest.cancel(false)
+ ? mPendingFillRequestId
+ : INVALID_REQUEST_ID;
+ }
+ }
+
+ /**
+ * The {@link AutofillId} which the client gets from its view is not contain the session id,
+ * but Autofill framework is using the {@link AutofillId} with a session id. So before using
+ * those ids in the Autofill framework, applies the current session id.
+ *
+ * @param res which response need to apply for a session id
+ */
+ private void processAutofillId(FillResponse res) {
+ if (res == null) {
+ return;
+ }
+
+ final List<Dataset> datasets = res.getDatasets();
+ if (datasets != null && !datasets.isEmpty()) {
+ for (int i = 0; i < datasets.size(); i++) {
+ final Dataset dataset = datasets.get(i);
+ if (dataset != null) {
+ applySessionId(dataset.getFieldIds());
+ }
+ }
+ }
+
+ final SaveInfo saveInfo = res.getSaveInfo();
+ if (saveInfo != null) {
+ applySessionId(saveInfo.getOptionalIds());
+ applySessionId(saveInfo.getRequiredIds());
+ applySessionId(saveInfo.getSanitizerValues());
+ applySessionId(saveInfo.getTriggerId());
+ }
+ }
+
+ private void applySessionId(List<AutofillId> ids) {
+ if (ids == null || ids.isEmpty()) {
+ return;
+ }
+
+ for (int i = 0; i < ids.size(); i++) {
+ applySessionId(ids.get(i));
+ }
+ }
+
+ private void applySessionId(AutofillId[][] ids) {
+ if (ids == null) {
+ return;
+ }
+ for (int i = 0; i < ids.length; i++) {
+ applySessionId(ids[i]);
+ }
+ }
+
+ private void applySessionId(AutofillId[] ids) {
+ if (ids == null) {
+ return;
+ }
+ for (int i = 0; i < ids.length; i++) {
+ applySessionId(ids[i]);
+ }
+ }
+
+ private void applySessionId(AutofillId id) {
+ if (id == null) {
+ return;
+ }
+ id.setSessionId(mSessionId);
+ }
+
+ private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) {
+ if (signal == null) {
+ return;
+ }
+ try {
+ signal.cancel();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error requesting a cancellation", e);
+ }
+ }
+
+ private class FillCallbackImpl extends IFillCallback.Stub {
+ final AndroidFuture<FillResponse> mFillRequest;
+ final AtomicReference<AndroidFuture<FillResponse>> mFutureRef;
+ final AtomicReference<ICancellationSignal> mCancellationSink;
+
+ FillCallbackImpl(AndroidFuture<FillResponse> fillRequest,
+ AtomicReference<AndroidFuture<FillResponse>> futureRef,
+ AtomicReference<ICancellationSignal> cancellationSink) {
+ mFillRequest = fillRequest;
+ mFutureRef = futureRef;
+ mCancellationSink = cancellationSink;
+ }
+
+ @Override
+ public void onCancellable(ICancellationSignal cancellation) {
+ AndroidFuture<FillResponse> future = mFutureRef.get();
+ if (future != null && future.isCancelled()) {
+ dispatchCancellationSignal(cancellation);
+ } else {
+ mCancellationSink.set(cancellation);
+ }
+ }
+
+ @Override
+ public void onSuccess(FillResponse response) {
+ mFillRequest.complete(response);
+ }
+
+ @Override
+ public void onFailure(int requestId, CharSequence message) {
+ String errorMessage = message == null ? "" : String.valueOf(message);
+ mFillRequest.completeExceptionally(
+ new RuntimeException(errorMessage));
+ }
+ }
+}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 2aa5d263e771..b7f736e4dc33 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -26,6 +26,7 @@ import static android.view.autofill.AutofillManager.ACTION_START_SESSION;
import static android.view.autofill.AutofillManager.ACTION_VALUE_CHANGED;
import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED;
import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED;
+import static android.view.autofill.AutofillManager.FLAG_ENABLED_CLIENT_SUGGESTIONS;
import static android.view.autofill.AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM;
import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString;
@@ -53,6 +54,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -346,6 +348,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
*/
private final AssistDataReceiverImpl mAssistReceiver = new AssistDataReceiverImpl();
+ @Nullable
+ private ClientSuggestionsSession mClientSuggestionsSession;
+
void onSwitchInputMethodLocked() {
// One caveat is that for the case where the focus is on a field for which regular autofill
// returns null, and augmented autofill is triggered, and then the user switches the input
@@ -416,6 +421,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
/** Whether the current {@link FillResponse} is expired. */
@GuardedBy("mLock")
private boolean mExpiredResponse;
+
+ /** Whether the client is using {@link android.view.autofill.AutofillRequestCallback}. */
+ @GuardedBy("mLock")
+ private boolean mClientSuggestionsEnabled;
}
/**
@@ -728,30 +737,39 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
/**
- * Cancels the last request sent to the {@link #mRemoteFillService}.
+ * Cancels the last request sent to the {@link #mRemoteFillService} or the
+ * {@link #mClientSuggestionsSession}.
*/
@GuardedBy("mLock")
private void cancelCurrentRequestLocked() {
- if (mRemoteFillService == null) {
- wtf(null, "cancelCurrentRequestLocked() called without a remote service. "
- + "mForAugmentedAutofillOnly: %s", mSessionFlags.mAugmentedAutofillOnly);
+ if (mRemoteFillService == null && mClientSuggestionsSession == null) {
+ wtf(null, "cancelCurrentRequestLocked() called without a remote service or a "
+ + "client suggestions session. mForAugmentedAutofillOnly: %s",
+ mSessionFlags.mAugmentedAutofillOnly);
return;
}
- final int canceledRequest = mRemoteFillService.cancelCurrentRequest();
- // Remove the FillContext as there will never be a response for the service
- if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) {
- final int numContexts = mContexts.size();
+ if (mRemoteFillService != null) {
+ final int canceledRequest = mRemoteFillService.cancelCurrentRequest();
- // It is most likely the last context, hence search backwards
- for (int i = numContexts - 1; i >= 0; i--) {
- if (mContexts.get(i).getRequestId() == canceledRequest) {
- if (sDebug) Slog.d(TAG, "cancelCurrentRequest(): id = " + canceledRequest);
- mContexts.remove(i);
- break;
+ // Remove the FillContext as there will never be a response for the service
+ if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) {
+ final int numContexts = mContexts.size();
+
+ // It is most likely the last context, hence search backwards
+ for (int i = numContexts - 1; i >= 0; i--) {
+ if (mContexts.get(i).getRequestId() == canceledRequest) {
+ if (sDebug) Slog.d(TAG, "cancelCurrentRequest(): id = " + canceledRequest);
+ mContexts.remove(i);
+ break;
+ }
}
}
}
+
+ if (mClientSuggestionsSession != null) {
+ mClientSuggestionsSession.cancelCurrentRequest();
+ }
}
private boolean isViewFocusedLocked(int flags) {
@@ -816,17 +834,30 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// structure is taken. This causes only one fill request per burst of focus changes.
cancelCurrentRequestLocked();
- // Only ask IME to create inline suggestions request if Autofill provider supports it and
- // the render service is available except the autofill is triggered manually and the view
- // is also not focused.
+ // Only ask IME to create inline suggestions request when
+ // 1. Autofill provider supports it or client enabled client suggestions.
+ // 2. The render service is available.
+ // 3. The view is focused. (The view may not be focused if the autofill is triggered
+ // manually.)
final RemoteInlineSuggestionRenderService remoteRenderService =
mService.getRemoteInlineSuggestionRenderServiceLocked();
- if (mSessionFlags.mInlineSupportedByService
+ if ((mSessionFlags.mInlineSupportedByService || mSessionFlags.mClientSuggestionsEnabled)
&& remoteRenderService != null
&& isViewFocusedLocked(flags)) {
- Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer =
- mAssistReceiver.newAutofillRequestLocked(viewState,
- /* isInlineRequest= */ true);
+ Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer;
+ if (mSessionFlags.mClientSuggestionsEnabled) {
+ final int finalRequestId = requestId;
+ inlineSuggestionsRequestConsumer = (inlineSuggestionsRequest) -> {
+ // Using client suggestions
+ synchronized (mLock) {
+ onClientFillRequestLocked(finalRequestId, inlineSuggestionsRequest);
+ }
+ viewState.resetState(ViewState.STATE_PENDING_CREATE_INLINE_REQUEST);
+ };
+ } else {
+ inlineSuggestionsRequestConsumer = mAssistReceiver.newAutofillRequestLocked(
+ viewState, /* isInlineRequest= */ true);
+ }
if (inlineSuggestionsRequestConsumer != null) {
final AutofillId focusedId = mCurrentViewId;
final int requestIdCopy = requestId;
@@ -842,10 +873,18 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
);
viewState.setState(ViewState.STATE_PENDING_CREATE_INLINE_REQUEST);
}
+ } else if (mSessionFlags.mClientSuggestionsEnabled) {
+ // Request client suggestions for the dropdown mode
+ onClientFillRequestLocked(requestId, null);
} else {
mAssistReceiver.newAutofillRequestLocked(viewState, /* isInlineRequest= */ false);
}
+ if (mSessionFlags.mClientSuggestionsEnabled) {
+ // Using client suggestions, unnecessary request AssistStructure
+ return;
+ }
+
// Now request the assist structure data.
try {
final Bundle receiverExtras = new Bundle();
@@ -895,10 +934,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mComponentName = componentName;
mCompatMode = compatMode;
mSessionState = STATE_ACTIVE;
+
synchronized (mLock) {
mSessionFlags = new SessionFlags();
mSessionFlags.mAugmentedAutofillOnly = forAugmentedAutofillOnly;
mSessionFlags.mInlineSupportedByService = mService.isInlineSuggestionsEnabledLocked();
+ mSessionFlags.mClientSuggestionsEnabled =
+ (mFlags & FLAG_ENABLED_CLIENT_SUGGESTIONS) != 0;
setClientLocked(client);
}
@@ -3030,13 +3072,22 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
filterText = value.getTextValue().toString();
}
- final CharSequence serviceLabel;
- final Drawable serviceIcon;
+ final CharSequence targetLabel;
+ final Drawable targetIcon;
synchronized (mLock) {
- serviceLabel = mService.getServiceLabelLocked();
- serviceIcon = mService.getServiceIconLocked();
+ if (mSessionFlags.mClientSuggestionsEnabled) {
+ final ApplicationInfo appInfo = ClientSuggestionsSession.getAppInfo(mComponentName,
+ mService.getUserId());
+ targetLabel = ClientSuggestionsSession.getAppLabelLocked(
+ mService.getMaster().getContext(), appInfo);
+ targetIcon = ClientSuggestionsSession.getAppIconLocked(
+ mService.getMaster().getContext(), appInfo);
+ } else {
+ targetLabel = mService.getServiceLabelLocked();
+ targetIcon = mService.getServiceIconLocked();
+ }
}
- if (serviceLabel == null || serviceIcon == null) {
+ if (targetLabel == null || targetIcon == null) {
wtf(null, "onFillReady(): no service label or icon");
return;
}
@@ -3056,7 +3107,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
getUiForShowing().showFillUi(filledId, response, filterText,
mService.getServicePackageName(), mComponentName,
- serviceLabel, serviceIcon, this, id, mCompatMode);
+ targetLabel, targetIcon, this, id, mCompatMode);
mService.logDatasetShown(id, mClientState);
@@ -3712,6 +3763,21 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
+ @GuardedBy("mLock")
+ private void onClientFillRequestLocked(int requestId,
+ InlineSuggestionsRequest inlineSuggestionsRequest) {
+ if (mClientSuggestionsSession == null) {
+ mClientSuggestionsSession = new ClientSuggestionsSession(id, mClient, mHandler,
+ mComponentName, this);
+ }
+
+ if (mContexts == null) {
+ mContexts = new ArrayList<>(1);
+ }
+
+ mClientSuggestionsSession.onFillRequest(requestId, inlineSuggestionsRequest, mFlags);
+ }
+
/**
* The result of checking whether to show the save dialog, when session can be saved.
*