diff options
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. * |