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 static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 22 23 import android.annotation.BinderThread; 24 import android.annotation.MainThread; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.IBinder; 30 import android.os.RemoteException; 31 import android.util.Log; 32 import android.view.autofill.AutofillId; 33 import android.view.inputmethod.InlineSuggestionsRequest; 34 import android.view.inputmethod.InlineSuggestionsResponse; 35 36 import com.android.internal.view.IInlineSuggestionsRequestCallback; 37 import com.android.internal.view.IInlineSuggestionsResponseCallback; 38 import com.android.internal.view.InlineSuggestionsRequestInfo; 39 40 import java.lang.ref.WeakReference; 41 import java.util.Collections; 42 import java.util.function.Consumer; 43 import java.util.function.Function; 44 import java.util.function.Supplier; 45 46 /** 47 * Maintains an inline suggestion session with the autofill manager service. 48 * 49 * <p> Each session correspond to one request from the Autofill manager service to create an 50 * {@link InlineSuggestionsRequest}. It's responsible for calling back to the Autofill manager 51 * service with {@link InlineSuggestionsRequest} and receiving {@link InlineSuggestionsResponse} 52 * from it. 53 * <p> 54 * TODO(b/151123764): currently the session may receive responses for different views on the same 55 * screen, but we will fix it so each session corresponds to one view. 56 * 57 * <p> All the methods are expected to be called from the main thread, to ensure thread safety. 58 */ 59 class InlineSuggestionSession { 60 private static final String TAG = "ImsInlineSuggestionSession"; 61 62 static final InlineSuggestionsResponse EMPTY_RESPONSE = new InlineSuggestionsResponse( 63 Collections.emptyList()); 64 65 @NonNull 66 private final Handler mMainThreadHandler; 67 @NonNull 68 private final InlineSuggestionSessionController mInlineSuggestionSessionController; 69 @NonNull 70 private final InlineSuggestionsRequestInfo mRequestInfo; 71 @NonNull 72 private final IInlineSuggestionsRequestCallback mCallback; 73 @NonNull 74 private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier; 75 @NonNull 76 private final Supplier<IBinder> mHostInputTokenSupplier; 77 @NonNull 78 private final Consumer<InlineSuggestionsResponse> mResponseConsumer; 79 // Indicate whether the previous call to the mResponseConsumer is empty or not. If it hasn't 80 // been called yet, the value would be null. 81 @Nullable 82 private Boolean mPreviousResponseIsEmpty; 83 84 85 /** 86 * Indicates whether {@link #makeInlineSuggestionRequestUncheck()} has been called or not, 87 * because it should only be called at most once. 88 */ 89 @Nullable 90 private boolean mCallbackInvoked = false; 91 @Nullable 92 private InlineSuggestionsResponseCallbackImpl mResponseCallback; 93 InlineSuggestionSession(@onNull InlineSuggestionsRequestInfo requestInfo, @NonNull IInlineSuggestionsRequestCallback callback, @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier, @NonNull Supplier<IBinder> hostInputTokenSupplier, @NonNull Consumer<InlineSuggestionsResponse> responseConsumer, @NonNull InlineSuggestionSessionController inlineSuggestionSessionController, @NonNull Handler mainThreadHandler)94 InlineSuggestionSession(@NonNull InlineSuggestionsRequestInfo requestInfo, 95 @NonNull IInlineSuggestionsRequestCallback callback, 96 @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier, 97 @NonNull Supplier<IBinder> hostInputTokenSupplier, 98 @NonNull Consumer<InlineSuggestionsResponse> responseConsumer, 99 @NonNull InlineSuggestionSessionController inlineSuggestionSessionController, 100 @NonNull Handler mainThreadHandler) { 101 mRequestInfo = requestInfo; 102 mCallback = callback; 103 mRequestSupplier = requestSupplier; 104 mHostInputTokenSupplier = hostInputTokenSupplier; 105 mResponseConsumer = responseConsumer; 106 mInlineSuggestionSessionController = inlineSuggestionSessionController; 107 mMainThreadHandler = mainThreadHandler; 108 } 109 110 @MainThread getRequestInfo()111 InlineSuggestionsRequestInfo getRequestInfo() { 112 return mRequestInfo; 113 } 114 115 @MainThread getRequestCallback()116 IInlineSuggestionsRequestCallback getRequestCallback() { 117 return mCallback; 118 } 119 120 /** 121 * Returns true if the session should send Ime status updates to Autofill. 122 * 123 * <p> The session only starts to send Ime status updates to Autofill after the sending back 124 * an {@link InlineSuggestionsRequest}. 125 */ 126 @MainThread shouldSendImeStatus()127 boolean shouldSendImeStatus() { 128 return mResponseCallback != null; 129 } 130 131 /** 132 * Returns true if {@link #makeInlineSuggestionRequestUncheck()} is called. It doesn't not 133 * necessarily mean an {@link InlineSuggestionsRequest} was sent, because it may call {@link 134 * IInlineSuggestionsRequestCallback#onInlineSuggestionsUnsupported()}. 135 * 136 * <p> The callback should be invoked at most once for each session. 137 */ 138 @MainThread isCallbackInvoked()139 boolean isCallbackInvoked() { 140 return mCallbackInvoked; 141 } 142 143 /** 144 * Invalidates the current session so it doesn't process any further {@link 145 * InlineSuggestionsResponse} from Autofill. 146 * 147 * <p> This method should be called when the session is de-referenced from the {@link 148 * InlineSuggestionSessionController}. 149 */ 150 @MainThread invalidate()151 void invalidate() { 152 try { 153 mCallback.onInlineSuggestionsSessionInvalidated(); 154 } catch (RemoteException e) { 155 Log.w(TAG, "onInlineSuggestionsSessionInvalidated() remote exception", e); 156 } 157 if (mResponseCallback != null) { 158 consumeInlineSuggestionsResponse(EMPTY_RESPONSE); 159 mResponseCallback.invalidate(); 160 mResponseCallback = null; 161 } 162 } 163 164 /** 165 * Gets the {@link InlineSuggestionsRequest} from IME and send it back to the Autofill if it's 166 * not null. 167 * 168 * <p>Calling this method implies that the input is started on the view corresponding to the 169 * session. 170 */ 171 @MainThread makeInlineSuggestionRequestUncheck()172 void makeInlineSuggestionRequestUncheck() { 173 if (mCallbackInvoked) { 174 return; 175 } 176 try { 177 final InlineSuggestionsRequest request = mRequestSupplier.apply( 178 mRequestInfo.getUiExtras()); 179 if (request == null) { 180 if (DEBUG) { 181 Log.d(TAG, "onCreateInlineSuggestionsRequest() returned null request"); 182 } 183 mCallback.onInlineSuggestionsUnsupported(); 184 } else { 185 request.setHostInputToken(mHostInputTokenSupplier.get()); 186 request.filterContentTypes(); 187 mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this); 188 mCallback.onInlineSuggestionsRequest(request, mResponseCallback); 189 } 190 } catch (RemoteException e) { 191 Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e); 192 } 193 mCallbackInvoked = true; 194 } 195 196 @MainThread handleOnInlineSuggestionsResponse(@onNull AutofillId fieldId, @NonNull InlineSuggestionsResponse response)197 void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId, 198 @NonNull InlineSuggestionsResponse response) { 199 if (!mInlineSuggestionSessionController.match(fieldId)) { 200 return; 201 } 202 if (DEBUG) { 203 Log.d(TAG, "IME receives response: " + response.getInlineSuggestions().size()); 204 } 205 consumeInlineSuggestionsResponse(response); 206 } 207 208 @MainThread consumeInlineSuggestionsResponse(@onNull InlineSuggestionsResponse response)209 void consumeInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) { 210 boolean isResponseEmpty = response.getInlineSuggestions().isEmpty(); 211 if (isResponseEmpty && Boolean.TRUE.equals(mPreviousResponseIsEmpty)) { 212 // No-op if both the previous response and current response are empty. 213 return; 214 } 215 mPreviousResponseIsEmpty = isResponseEmpty; 216 mResponseConsumer.accept(response); 217 } 218 219 /** 220 * Internal implementation of {@link IInlineSuggestionsResponseCallback}. 221 */ 222 private static final class InlineSuggestionsResponseCallbackImpl extends 223 IInlineSuggestionsResponseCallback.Stub { 224 private final WeakReference<InlineSuggestionSession> mSession; 225 private volatile boolean mInvalid = false; 226 InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session)227 private InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session) { 228 mSession = new WeakReference<>(session); 229 } 230 invalidate()231 void invalidate() { 232 mInvalid = true; 233 } 234 235 @BinderThread 236 @Override onInlineSuggestionsResponse(AutofillId fieldId, InlineSuggestionsResponse response)237 public void onInlineSuggestionsResponse(AutofillId fieldId, 238 InlineSuggestionsResponse response) { 239 if (mInvalid) { 240 return; 241 } 242 final InlineSuggestionSession session = mSession.get(); 243 if (session != null) { 244 session.mMainThreadHandler.sendMessage( 245 obtainMessage(InlineSuggestionSession::handleOnInlineSuggestionsResponse, 246 session, fieldId, response)); 247 } 248 } 249 } 250 } 251