/* * 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.Collections; 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. * *
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. *
* 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. * *
All the methods are expected to be called from the main thread, to ensure thread safety.
*/
class InlineSuggestionSession {
private static final String TAG = "ImsInlineSuggestionSession";
static final InlineSuggestionsResponse EMPTY_RESPONSE = new InlineSuggestionsResponse(
Collections.emptyList());
@NonNull
private final Handler mMainThreadHandler;
@NonNull
private final InlineSuggestionSessionController mInlineSuggestionSessionController;
@NonNull
private final InlineSuggestionsRequestInfo mRequestInfo;
@NonNull
private final IInlineSuggestionsRequestCallback mCallback;
@NonNull
private final Function 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()}.
*
* 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.
*
* This method should be called when the session is de-referenced from the {@link
* InlineSuggestionSessionController}.
*/
@MainThread
void invalidate() {
try {
mCallback.onInlineSuggestionsSessionInvalidated();
} catch (RemoteException e) {
Log.w(TAG, "onInlineSuggestionsSessionInvalidated() remote exception", e);
}
if (mResponseCallback != null) {
consumeInlineSuggestionsResponse(EMPTY_RESPONSE);
mResponseCallback.invalidate();
mResponseCallback = null;
}
}
/**
* Gets the {@link InlineSuggestionsRequest} from IME and send it back to the Autofill if it's
* not null.
*
* 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());
}
consumeInlineSuggestionsResponse(response);
}
@MainThread
void consumeInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) {
boolean isResponseEmpty = response.getInlineSuggestions().isEmpty();
if (isResponseEmpty && Boolean.TRUE.equals(mPreviousResponseIsEmpty)) {
// No-op if both the previous response and current response are empty.
return;
}
mPreviousResponseIsEmpty = isResponseEmpty;
mResponseConsumer.accept(response);
}
/**
* Internal implementation of {@link IInlineSuggestionsResponseCallback}.
*/
private static final class InlineSuggestionsResponseCallbackImpl extends
IInlineSuggestionsResponseCallback.Stub {
private final WeakReference