blob: c31cb4e61b6e268dcc0e54bd0b930af14c17b3fd [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.EditorInfo;
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.
*
* <p>
* The data flow from IMS point of view is:
* Before calling {@link InputMethodService#onStartInputView(EditorInfo, boolean)}, call the {@link
* #notifyOnStartInputView(AutofillId)}
* ->
* [async] {@link IInlineSuggestionsRequestCallback#onInputMethodStartInputView(AutofillId)}
* --- process boundary ---
* ->
* {@link com.android.server.inputmethod.InputMethodManagerService
* .InlineSuggestionsRequestCallbackDecorator#onInputMethodStartInputView(AutofillId)}
* ->
* {@link com.android.server.autofill.InlineSuggestionSession
* .InlineSuggestionsRequestCallbackImpl#onInputMethodStartInputView(AutofillId)}
*
* <p>
* The data flow for {@link #notifyOnFinishInputView(AutofillId)} is similar.
*/
class InlineSuggestionSession {
private static final String TAG = "ImsInlineSuggestionSession";
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,
boolean inputViewStarted) {
mComponentName = componentName;
mCallback = callback;
mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this);
mClientPackageNameSupplier = clientPackageNameSupplier;
mClientAutofillIdSupplier = clientAutofillIdSupplier;
mRequestSupplier = requestSupplier;
mHostInputTokenSupplier = hostInputTokenSupplier;
mResponseConsumer = responseConsumer;
makeInlineSuggestionsRequest(inputViewStarted);
}
void notifyOnStartInputView(AutofillId imeFieldId) {
if (DEBUG) Log.d(TAG, "notifyOnStartInputView");
try {
mCallback.onInputMethodStartInputView(imeFieldId);
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
}
}
void notifyOnFinishInputView(AutofillId imeFieldId) {
if (DEBUG) Log.d(TAG, "notifyOnFinishInputView");
try {
mCallback.onInputMethodFinishInputView(imeFieldId);
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodFinishInputView() remote exception:" + e);
}
}
/**
* 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(boolean inputViewStarted) {
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,
mClientAutofillIdSupplier.get(), inputViewStarted);
}
} 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;
}
if (!mComponentName.getPackageName().equals(mClientPackageNameSupplier.get())
|| !fieldId.equalsIgnoreSession(mClientAutofillIdSupplier.get())) {
if (DEBUG) {
Log.d(TAG,
"handleOnInlineSuggestionsResponse() called on the wrong package/field "
+ "name: " + mComponentName.getPackageName() + " v.s. "
+ mClientPackageNameSupplier.get() + ", " + fieldId + " v.s. "
+ mClientAutofillIdSupplier.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));
}
}
}
}