• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package android.inputmethodservice;
18 
19 import static android.inputmethodservice.InputMethodService.DEBUG;
20 
21 import android.annotation.MainThread;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.IBinder;
27 import android.os.Looper;
28 import android.os.RemoteException;
29 import android.util.Log;
30 import android.view.autofill.AutofillId;
31 import android.view.inputmethod.EditorInfo;
32 import android.view.inputmethod.InlineSuggestionsRequest;
33 import android.view.inputmethod.InlineSuggestionsResponse;
34 
35 import com.android.internal.view.IInlineSuggestionsRequestCallback;
36 import com.android.internal.view.InlineSuggestionsRequestInfo;
37 
38 import java.util.function.Consumer;
39 import java.util.function.Function;
40 import 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  */
59 class 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 
InlineSuggestionSessionController( @onNull Function<Bundle, InlineSuggestionsRequest> requestSupplier, @NonNull Supplier<IBinder> hostInputTokenSupplier, @NonNull Consumer<InlineSuggestionsResponse> responseConsumer)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
onMakeInlineSuggestionsRequest(@onNull InlineSuggestionsRequestInfo requestInfo, @NonNull IInlineSuggestionsRequestCallback callback)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
notifyOnStartInput(@ullable String imeClientPackageName, @Nullable AutofillId imeFieldId)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             mSession.consumeInlineSuggestionsResponse(InlineSuggestionSession.EMPTY_RESPONSE);
138             // Initiates the callback to Autofill if there is a pending matching session.
139             // Otherwise updates the session with the Ime status.
140             if (!mSession.isCallbackInvoked() && match(mSession.getRequestInfo())) {
141                 mSession.makeInlineSuggestionRequestUncheck();
142             } else if (mSession.shouldSendImeStatus()) {
143                 try {
144                     mSession.getRequestCallback().onInputMethodStartInput(mImeClientFieldId);
145                 } catch (RemoteException e) {
146                     Log.w(TAG, "onInputMethodStartInput() remote exception:" + e);
147                 }
148             }
149         }
150     }
151 
152     /**
153      * Called from IME main thread after getting results from
154      * {@link InputMethodService#dispatchOnShowInputRequested(int,
155      * boolean)}. This method should be quick as it makes a unblocking IPC.
156      */
157     @MainThread
notifyOnShowInputRequested(boolean requestResult)158     void notifyOnShowInputRequested(boolean requestResult) {
159         if (DEBUG) Log.d(TAG, "notifyShowInputRequested");
160         if (mSession != null && mSession.shouldSendImeStatus()) {
161             try {
162                 mSession.getRequestCallback().onInputMethodShowInputRequested(requestResult);
163             } catch (RemoteException e) {
164                 Log.w(TAG, "onInputMethodShowInputRequested() remote exception:" + e);
165             }
166         }
167     }
168 
169     /**
170      * Called from IME main thread before calling
171      * {@link InputMethodService#onStartInputView(EditorInfo,
172      * boolean)} . This method should be quick as it makes a unblocking IPC.
173      */
174     @MainThread
notifyOnStartInputView()175     void notifyOnStartInputView() {
176         if (DEBUG) Log.d(TAG, "notifyOnStartInputView");
177         mImeInputViewStarted = true;
178         if (mSession != null && mSession.shouldSendImeStatus()) {
179             try {
180                 mSession.getRequestCallback().onInputMethodStartInputView();
181             } catch (RemoteException e) {
182                 Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
183             }
184         }
185     }
186 
187     /**
188      * Called from IME main thread before calling
189      * {@link InputMethodService#onFinishInputView(boolean)}.
190      * This method should be quick as it makes a unblocking IPC.
191      */
192     @MainThread
notifyOnFinishInputView()193     void notifyOnFinishInputView() {
194         if (DEBUG) Log.d(TAG, "notifyOnFinishInputView");
195         mImeInputViewStarted = false;
196         if (mSession != null && mSession.shouldSendImeStatus()) {
197             try {
198                 mSession.getRequestCallback().onInputMethodFinishInputView();
199             } catch (RemoteException e) {
200                 Log.w(TAG, "onInputMethodFinishInputView() remote exception:" + e);
201             }
202         }
203     }
204 
205     /**
206      * Called from IME main thread before calling {@link InputMethodService#onFinishInput()}. This
207      * method should be quick as it makes a unblocking IPC.
208      */
209     @MainThread
notifyOnFinishInput()210     void notifyOnFinishInput() {
211         if (DEBUG) Log.d(TAG, "notifyOnFinishInput");
212         mImeClientPackageName = null;
213         mImeClientFieldId = null;
214         mImeInputViewStarted = false;
215         mImeInputStarted = false;
216         if (mSession != null && mSession.shouldSendImeStatus()) {
217             try {
218                 mSession.getRequestCallback().onInputMethodFinishInput();
219             } catch (RemoteException e) {
220                 Log.w(TAG, "onInputMethodFinishInput() remote exception:" + e);
221             }
222         }
223     }
224 
225     /**
226      * Returns true if the current Ime focused field matches the session {@code requestInfo}.
227      */
228     @MainThread
match(@ullable InlineSuggestionsRequestInfo requestInfo)229     boolean match(@Nullable InlineSuggestionsRequestInfo requestInfo) {
230         return match(requestInfo, mImeClientPackageName, mImeClientFieldId);
231     }
232 
233     /**
234      * Returns true if the current Ime focused field matches the {@code autofillId}.
235      */
236     @MainThread
match(@ullable AutofillId autofillId)237     boolean match(@Nullable AutofillId autofillId) {
238         return match(autofillId, mImeClientFieldId);
239     }
240 
match( @ullable InlineSuggestionsRequestInfo inlineSuggestionsRequestInfo, @Nullable String imeClientPackageName, @Nullable AutofillId imeClientFieldId)241     private static boolean match(
242             @Nullable InlineSuggestionsRequestInfo inlineSuggestionsRequestInfo,
243             @Nullable String imeClientPackageName, @Nullable AutofillId imeClientFieldId) {
244         if (inlineSuggestionsRequestInfo == null || imeClientPackageName == null
245                 || imeClientFieldId == null) {
246             return false;
247         }
248         return inlineSuggestionsRequestInfo.getComponentName().getPackageName().equals(
249                 imeClientPackageName) && match(inlineSuggestionsRequestInfo.getAutofillId(),
250                 imeClientFieldId);
251     }
252 
match(@ullable AutofillId autofillId, @Nullable AutofillId imeClientFieldId)253     private static boolean match(@Nullable AutofillId autofillId,
254             @Nullable AutofillId imeClientFieldId) {
255         // The IME doesn't have information about the virtual view id for the child views in the
256         // web view, so we are only comparing the parent view id here. This means that for cases
257         // where there are two input fields in the web view, they will have the same view id
258         // (although different virtual child id), and we will not be able to distinguish them.
259         return autofillId != null && imeClientFieldId != null
260                 && autofillId.getViewId() == imeClientFieldId.getViewId();
261     }
262 }
263