blob: c9f9059bed4fc608a5e537effb9d89be11d8c768 [file] [log] [blame]
Feng Cao97ec1c42020-03-25 12:20:42 -07001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.inputmethodservice;
18
19import static android.inputmethodservice.InputMethodService.DEBUG;
20
21import android.annotation.MainThread;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.IBinder;
27import android.os.Looper;
28import android.os.RemoteException;
29import android.util.Log;
30import android.view.autofill.AutofillId;
31import android.view.inputmethod.EditorInfo;
32import android.view.inputmethod.InlineSuggestionsRequest;
33import android.view.inputmethod.InlineSuggestionsResponse;
34
35import com.android.internal.view.IInlineSuggestionsRequestCallback;
36import com.android.internal.view.InlineSuggestionsRequestInfo;
37
38import java.util.function.Consumer;
39import java.util.function.Function;
40import java.util.function.Supplier;
41
42/**
43 * Manages the interaction with the autofill manager service for the inline suggestion sessions.
44 *
45 * <p>
46 * The class maintains the inline suggestion session with the autofill service. There is at most one
47 * active inline suggestion session at any given time.
48 *
49 * <p>
50 * The class receives the IME status change events (input start/finish, input view start/finish, and
51 * show input requested result), and send them through IPC to the {@link
52 * com.android.server.inputmethod.InputMethodManagerService}, which sends them to {@link
53 * com.android.server.autofill.InlineSuggestionSession} in the Autofill manager service. If there is
54 * no open inline suggestion session, no event will be send to autofill manager service.
55 *
56 * <p>
57 * All the methods are expected to be called from the main thread, to ensure thread safety.
58 */
59class InlineSuggestionSessionController {
60 private static final String TAG = "InlineSuggestionSessionController";
61
62 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper(), null, true);
63
64 @NonNull
65 private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier;
66 @NonNull
67 private final Supplier<IBinder> mHostInputTokenSupplier;
68 @NonNull
69 private final Consumer<InlineSuggestionsResponse> mResponseConsumer;
70
71 /* The following variables track the IME status */
72 @Nullable
73 private String mImeClientPackageName;
74 @Nullable
75 private AutofillId mImeClientFieldId;
76 private boolean mImeInputStarted;
77 private boolean mImeInputViewStarted;
78
79 @Nullable
80 private InlineSuggestionSession mSession;
81
82 InlineSuggestionSessionController(
83 @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier,
84 @NonNull Supplier<IBinder> hostInputTokenSupplier,
85 @NonNull Consumer<InlineSuggestionsResponse> responseConsumer) {
86 mRequestSupplier = requestSupplier;
87 mHostInputTokenSupplier = hostInputTokenSupplier;
88 mResponseConsumer = responseConsumer;
89 }
90
91 /**
92 * Called upon IME receiving a create inline suggestion request. Must be called in the main
93 * thread to ensure thread safety.
94 */
95 @MainThread
96 void onMakeInlineSuggestionsRequest(@NonNull InlineSuggestionsRequestInfo requestInfo,
97 @NonNull IInlineSuggestionsRequestCallback callback) {
98 if (DEBUG) Log.d(TAG, "onMakeInlineSuggestionsRequest: " + requestInfo);
99 // Creates a new session for the new create request from Autofill.
100 if (mSession != null) {
101 mSession.invalidate();
102 }
103 mSession = new InlineSuggestionSession(requestInfo, callback, mRequestSupplier,
104 mHostInputTokenSupplier, mResponseConsumer, this, mMainThreadHandler);
105
106 // If the input is started on the same view, then initiate the callback to the Autofill.
107 // Otherwise wait for the input to start.
108 if (mImeInputStarted && match(mSession.getRequestInfo())) {
109 mSession.makeInlineSuggestionRequestUncheck();
110 // ... then update the Autofill whether the input view is started.
111 if (mImeInputViewStarted) {
112 try {
113 mSession.getRequestCallback().onInputMethodStartInputView();
114 } catch (RemoteException e) {
115 Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
116 }
117 }
118 }
119 }
120
121 /**
122 * Called from IME main thread before calling {@link InputMethodService#onStartInput(EditorInfo,
123 * boolean)}. This method should be quick as it makes a unblocking IPC.
124 */
125 @MainThread
126 void notifyOnStartInput(@Nullable String imeClientPackageName,
127 @Nullable AutofillId imeFieldId) {
128 if (DEBUG) Log.d(TAG, "notifyOnStartInput: " + imeClientPackageName + ", " + imeFieldId);
129 if (imeClientPackageName == null || imeFieldId == null) {
130 return;
131 }
132 mImeInputStarted = true;
133 mImeClientPackageName = imeClientPackageName;
134 mImeClientFieldId = imeFieldId;
135
136 if (mSession != null) {
137 // Initiates the callback to Autofill if there is a pending matching session.
138 // Otherwise updates the session with the Ime status.
139 if (!mSession.isCallbackInvoked() && match(mSession.getRequestInfo())) {
140 mSession.makeInlineSuggestionRequestUncheck();
141 } else if (mSession.shouldSendImeStatus()) {
142 try {
143 mSession.getRequestCallback().onInputMethodStartInput(mImeClientFieldId);
144 } catch (RemoteException e) {
145 Log.w(TAG, "onInputMethodStartInput() remote exception:" + e);
146 }
147 }
148 }
149 }
150
151 /**
152 * Called from IME main thread after getting results from
153 * {@link InputMethodService#dispatchOnShowInputRequested(int,
154 * boolean)}. This method should be quick as it makes a unblocking IPC.
155 */
156 @MainThread
157 void notifyOnShowInputRequested(boolean requestResult) {
158 if (DEBUG) Log.d(TAG, "notifyShowInputRequested");
159 if (mSession != null && mSession.shouldSendImeStatus()) {
160 try {
161 mSession.getRequestCallback().onInputMethodShowInputRequested(requestResult);
162 } catch (RemoteException e) {
163 Log.w(TAG, "onInputMethodShowInputRequested() remote exception:" + e);
164 }
165 }
166 }
167
168 /**
169 * Called from IME main thread before calling
170 * {@link InputMethodService#onStartInputView(EditorInfo,
171 * boolean)} . This method should be quick as it makes a unblocking IPC.
172 */
173 @MainThread
174 void notifyOnStartInputView() {
175 if (DEBUG) Log.d(TAG, "notifyOnStartInputView");
176 mImeInputViewStarted = true;
177 if (mSession != null && mSession.shouldSendImeStatus()) {
178 try {
179 mSession.getRequestCallback().onInputMethodStartInputView();
180 } catch (RemoteException e) {
181 Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
182 }
183 }
184 }
185
186 /**
187 * Called from IME main thread before calling
188 * {@link InputMethodService#onFinishInputView(boolean)}.
189 * This method should be quick as it makes a unblocking IPC.
190 */
191 @MainThread
192 void notifyOnFinishInputView() {
193 if (DEBUG) Log.d(TAG, "notifyOnFinishInputView");
194 mImeInputViewStarted = false;
195 if (mSession != null && mSession.shouldSendImeStatus()) {
196 try {
197 mSession.getRequestCallback().onInputMethodFinishInputView();
198 } catch (RemoteException e) {
199 Log.w(TAG, "onInputMethodFinishInputView() remote exception:" + e);
200 }
201 }
202 }
203
204 /**
205 * Called from IME main thread before calling {@link InputMethodService#onFinishInput()}. This
206 * method should be quick as it makes a unblocking IPC.
207 */
208 @MainThread
209 void notifyOnFinishInput() {
210 if (DEBUG) Log.d(TAG, "notifyOnFinishInput");
211 mImeClientPackageName = null;
212 mImeClientFieldId = null;
213 mImeInputViewStarted = false;
214 mImeInputStarted = false;
215 if (mSession != null && mSession.shouldSendImeStatus()) {
216 try {
217 mSession.getRequestCallback().onInputMethodFinishInput();
218 } catch (RemoteException e) {
219 Log.w(TAG, "onInputMethodFinishInput() remote exception:" + e);
220 }
221 }
222 }
223
224 /**
225 * Returns true if the current Ime focused field matches the session {@code requestInfo}.
226 */
227 @MainThread
228 boolean match(@Nullable InlineSuggestionsRequestInfo requestInfo) {
229 return match(requestInfo, mImeClientPackageName, mImeClientFieldId);
230 }
231
232 /**
233 * Returns true if the current Ime focused field matches the {@code autofillId}.
234 */
235 @MainThread
236 boolean match(@Nullable AutofillId autofillId) {
237 return match(autofillId, mImeClientFieldId);
238 }
239
240 private static boolean match(
241 @Nullable InlineSuggestionsRequestInfo inlineSuggestionsRequestInfo,
242 @Nullable String imeClientPackageName, @Nullable AutofillId imeClientFieldId) {
243 if (inlineSuggestionsRequestInfo == null || imeClientPackageName == null
244 || imeClientFieldId == null) {
245 return false;
246 }
247 return inlineSuggestionsRequestInfo.getComponentName().getPackageName().equals(
248 imeClientPackageName) && match(inlineSuggestionsRequestInfo.getAutofillId(),
249 imeClientFieldId);
250 }
251
252 private static boolean match(@Nullable AutofillId autofillId,
253 @Nullable AutofillId imeClientFieldId) {
254 // The IME doesn't have information about the virtual view id for the child views in the
255 // web view, so we are only comparing the parent view id here. This means that for cases
256 // where there are two input fields in the web view, they will have the same view id
257 // (although different virtual child id), and we will not be able to distinguish them.
258 return autofillId != null && imeClientFieldId != null
259 && autofillId.getViewId() == imeClientFieldId.getViewId();
260 }
261}