blob: 25e90ee9833cd209fe28e1eb802ddaed27cfa2fc [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.NonNull;
import android.content.ComponentName;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
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 java.lang.ref.WeakReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Maintains an active inline suggestion session with the autofill manager service.
*
* <p>
* Each session corresponds to one {@link InlineSuggestionsRequest} and one {@link
* IInlineSuggestionsResponseCallback}, but there may be multiple invocations of the response
* callback for the same field or different fields in the same component.
*/
class InlineSuggestionSession {
private static final String TAG = InlineSuggestionSession.class.getSimpleName();
private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
@NonNull
private final ComponentName mComponentName;
@NonNull
private final IInlineSuggestionsRequestCallback mCallback;
@NonNull
private final InlineSuggestionsResponseCallbackImpl mResponseCallback;
@NonNull
private final Supplier<String> mClientPackageNameSupplier;
@NonNull
private final Supplier<AutofillId> mClientAutofillIdSupplier;
@NonNull
private final Supplier<InlineSuggestionsRequest> mRequestSupplier;
@NonNull
private final Supplier<IBinder> mHostInputTokenSupplier;
@NonNull
private final Consumer<InlineSuggestionsResponse> mResponseConsumer;
private volatile boolean mInvalidated = false;
InlineSuggestionSession(@NonNull ComponentName componentName,
@NonNull IInlineSuggestionsRequestCallback callback,
@NonNull Supplier<String> clientPackageNameSupplier,
@NonNull Supplier<AutofillId> clientAutofillIdSupplier,
@NonNull Supplier<InlineSuggestionsRequest> requestSupplier,
@NonNull Supplier<IBinder> hostInputTokenSupplier,
@NonNull Consumer<InlineSuggestionsResponse> responseConsumer) {
mComponentName = componentName;
mCallback = callback;
mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this);
mClientPackageNameSupplier = clientPackageNameSupplier;
mClientAutofillIdSupplier = clientAutofillIdSupplier;
mRequestSupplier = requestSupplier;
mHostInputTokenSupplier = hostInputTokenSupplier;
mResponseConsumer = responseConsumer;
makeInlineSuggestionsRequest();
}
/**
* This needs to be called before creating a new session, such that the later response callbacks
* will be discarded.
*/
void invalidateSession() {
mInvalidated = true;
}
/**
* Sends an {@link InlineSuggestionsRequest} obtained from {@cocde supplier} to the current
* Autofill Session through
* {@link IInlineSuggestionsRequestCallback#onInlineSuggestionsRequest}.
*/
private void makeInlineSuggestionsRequest() {
try {
final InlineSuggestionsRequest request = mRequestSupplier.get();
if (request == null) {
if (DEBUG) {
Log.d(TAG, "onCreateInlineSuggestionsRequest() returned null request");
}
mCallback.onInlineSuggestionsUnsupported();
} else {
request.setHostInputToken(mHostInputTokenSupplier.get());
mCallback.onInlineSuggestionsRequest(request, mResponseCallback);
}
} catch (RemoteException e) {
Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e);
}
}
private void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId,
@NonNull InlineSuggestionsResponse response) {
if (mInvalidated) {
if (DEBUG) {
Log.d(TAG, "handleOnInlineSuggestionsResponse() called on invalid session");
}
return;
}
// TODO(b/149522488): Verify fieldId against {@code mClientAutofillIdSupplier.get()} using
// {@link AutofillId#equalsIgnoreSession(AutofillId)}. Right now, this seems to be
// falsely alarmed quite often, depending whether autofill suggestions arrive earlier
// than the IMS EditorInfo updates or not.
if (!mComponentName.getPackageName().equals(mClientPackageNameSupplier.get())) {
if (DEBUG) {
Log.d(TAG,
"handleOnInlineSuggestionsResponse() called on the wrong package "
+ "name: " + mComponentName.getPackageName() + " v.s. "
+ mClientPackageNameSupplier.get());
}
return;
}
if (DEBUG) {
Log.d(TAG, "IME receives response: " + response.getInlineSuggestions().size());
}
mResponseConsumer.accept(response);
}
/**
* Internal implementation of {@link IInlineSuggestionsResponseCallback}.
*/
static final class InlineSuggestionsResponseCallbackImpl
extends IInlineSuggestionsResponseCallback.Stub {
private final WeakReference<InlineSuggestionSession> mInlineSuggestionSession;
private InlineSuggestionsResponseCallbackImpl(
InlineSuggestionSession inlineSuggestionSession) {
mInlineSuggestionSession = new WeakReference<>(inlineSuggestionSession);
}
@Override
public void onInlineSuggestionsResponse(AutofillId fieldId,
InlineSuggestionsResponse response) {
final InlineSuggestionSession session = mInlineSuggestionSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
InlineSuggestionSession::handleOnInlineSuggestionsResponse, session,
fieldId, response));
}
}
}
}