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