blob: 26197883c32fa03623996212956d349018c43977 [file] [log] [blame]
/*
* 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));
}
}
}
}