| /* |
| * 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.inputmethodservice; |
| |
| import static android.inputmethodservice.InputMethodService.DEBUG; |
| |
| import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; |
| |
| import android.annotation.BinderThread; |
| import android.annotation.MainThread; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.view.autofill.AutofillId; |
| import android.view.inputmethod.InlineSuggestionsRequest; |
| import android.view.inputmethod.InlineSuggestionsResponse; |
| |
| import com.android.internal.view.IInlineSuggestionsRequestCallback; |
| import com.android.internal.view.IInlineSuggestionsResponseCallback; |
| import com.android.internal.view.InlineSuggestionsRequestInfo; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| import java.util.function.Supplier; |
| |
| /** |
| * Maintains an inline suggestion session with the autofill manager service. |
| * |
| * <p> Each session correspond to one request from the Autofill manager service to create an |
| * {@link InlineSuggestionsRequest}. It's responsible for calling back to the Autofill manager |
| * service with {@link InlineSuggestionsRequest} and receiving {@link InlineSuggestionsResponse} |
| * from it. |
| * <p> |
| * TODO(b/151123764): currently the session may receive responses for different views on the same |
| * screen, but we will fix it so each session corresponds to one view. |
| * |
| * <p> All the methods are expected to be called from the main thread, to ensure thread safety. |
| */ |
| class InlineSuggestionSession { |
| private static final String TAG = "ImsInlineSuggestionSession"; |
| |
| @NonNull |
| private final Handler mMainThreadHandler; |
| @NonNull |
| private final InlineSuggestionSessionController mInlineSuggestionSessionController; |
| @NonNull |
| private final InlineSuggestionsRequestInfo mRequestInfo; |
| @NonNull |
| private final IInlineSuggestionsRequestCallback mCallback; |
| @NonNull |
| private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier; |
| @NonNull |
| private final Supplier<IBinder> mHostInputTokenSupplier; |
| @NonNull |
| private final Consumer<InlineSuggestionsResponse> mResponseConsumer; |
| |
| |
| /** |
| * Indicates whether {@link #makeInlineSuggestionRequestUncheck()} has been called or not, |
| * because it should only be called at most once. |
| */ |
| @Nullable |
| private boolean mCallbackInvoked = false; |
| @Nullable |
| private InlineSuggestionsResponseCallbackImpl mResponseCallback; |
| |
| InlineSuggestionSession(@NonNull InlineSuggestionsRequestInfo requestInfo, |
| @NonNull IInlineSuggestionsRequestCallback callback, |
| @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier, |
| @NonNull Supplier<IBinder> hostInputTokenSupplier, |
| @NonNull Consumer<InlineSuggestionsResponse> responseConsumer, |
| @NonNull InlineSuggestionSessionController inlineSuggestionSessionController, |
| @NonNull Handler mainThreadHandler) { |
| mRequestInfo = requestInfo; |
| mCallback = callback; |
| mRequestSupplier = requestSupplier; |
| mHostInputTokenSupplier = hostInputTokenSupplier; |
| mResponseConsumer = responseConsumer; |
| mInlineSuggestionSessionController = inlineSuggestionSessionController; |
| mMainThreadHandler = mainThreadHandler; |
| } |
| |
| @MainThread |
| InlineSuggestionsRequestInfo getRequestInfo() { |
| return mRequestInfo; |
| } |
| |
| @MainThread |
| IInlineSuggestionsRequestCallback getRequestCallback() { |
| return mCallback; |
| } |
| |
| /** |
| * Returns true if the session should send Ime status updates to Autofill. |
| * |
| * <p> The session only starts to send Ime status updates to Autofill after the sending back |
| * an {@link InlineSuggestionsRequest}. |
| */ |
| @MainThread |
| boolean shouldSendImeStatus() { |
| return mResponseCallback != null; |
| } |
| |
| /** |
| * Returns true if {@link #makeInlineSuggestionRequestUncheck()} is called. It doesn't not |
| * necessarily mean an {@link InlineSuggestionsRequest} was sent, because it may call {@link |
| * IInlineSuggestionsRequestCallback#onInlineSuggestionsUnsupported()}. |
| * |
| * <p> The callback should be invoked at most once for each session. |
| */ |
| @MainThread |
| boolean isCallbackInvoked() { |
| return mCallbackInvoked; |
| } |
| |
| /** |
| * Invalidates the current session so it doesn't process any further {@link |
| * InlineSuggestionsResponse} from Autofill. |
| * |
| * <p> This method should be called when the session is de-referenced from the {@link |
| * InlineSuggestionSessionController}. |
| */ |
| @MainThread |
| void invalidate() { |
| if (mResponseCallback != null) { |
| mResponseCallback.invalidate(); |
| mResponseCallback = null; |
| } |
| } |
| |
| /** |
| * Gets the {@link InlineSuggestionsRequest} from IME and send it back to the Autofill if it's |
| * not null. |
| * |
| * <p>Calling this method implies that the input is started on the view corresponding to the |
| * session. |
| */ |
| @MainThread |
| void makeInlineSuggestionRequestUncheck() { |
| if (mCallbackInvoked) { |
| return; |
| } |
| try { |
| final InlineSuggestionsRequest request = mRequestSupplier.apply( |
| mRequestInfo.getUiExtras()); |
| if (request == null) { |
| if (DEBUG) { |
| Log.d(TAG, "onCreateInlineSuggestionsRequest() returned null request"); |
| } |
| mCallback.onInlineSuggestionsUnsupported(); |
| } else { |
| request.setHostInputToken(mHostInputTokenSupplier.get()); |
| request.filterContentTypes(); |
| mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this); |
| mCallback.onInlineSuggestionsRequest(request, mResponseCallback); |
| } |
| } catch (RemoteException e) { |
| Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e); |
| } |
| mCallbackInvoked = true; |
| } |
| |
| @MainThread |
| void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId, |
| @NonNull InlineSuggestionsResponse response) { |
| if (!mInlineSuggestionSessionController.match(fieldId)) { |
| return; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "IME receives response: " + response.getInlineSuggestions().size()); |
| } |
| mResponseConsumer.accept(response); |
| } |
| |
| /** |
| * Internal implementation of {@link IInlineSuggestionsResponseCallback}. |
| */ |
| private static final class InlineSuggestionsResponseCallbackImpl extends |
| IInlineSuggestionsResponseCallback.Stub { |
| private final WeakReference<InlineSuggestionSession> mSession; |
| private volatile boolean mInvalid = false; |
| |
| private InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session) { |
| mSession = new WeakReference<>(session); |
| } |
| |
| void invalidate() { |
| mInvalid = true; |
| } |
| |
| @BinderThread |
| @Override |
| public void onInlineSuggestionsResponse(AutofillId fieldId, |
| InlineSuggestionsResponse response) { |
| if (mInvalid) { |
| return; |
| } |
| final InlineSuggestionSession session = mSession.get(); |
| if (session != null) { |
| session.mMainThreadHandler.sendMessage( |
| obtainMessage(InlineSuggestionSession::handleOnInlineSuggestionsResponse, |
| session, fieldId, response)); |
| } |
| } |
| } |
| } |