SessionTextInput.java (15835B)
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.geckoview; 7 8 import android.content.Context; 9 import android.graphics.RectF; 10 import android.os.Handler; 11 import android.text.Editable; 12 import android.util.Log; 13 import android.view.KeyEvent; 14 import android.view.View; 15 import android.view.inputmethod.CursorAnchorInfo; 16 import android.view.inputmethod.EditorInfo; 17 import android.view.inputmethod.ExtractedText; 18 import android.view.inputmethod.ExtractedTextRequest; 19 import android.view.inputmethod.InputConnection; 20 import android.view.inputmethod.InputMethodManager; 21 import androidx.annotation.AnyThread; 22 import androidx.annotation.IntDef; 23 import androidx.annotation.NonNull; 24 import androidx.annotation.Nullable; 25 import androidx.annotation.UiThread; 26 import java.lang.annotation.Retention; 27 import java.lang.annotation.RetentionPolicy; 28 import org.mozilla.gecko.IGeckoEditableParent; 29 import org.mozilla.gecko.InputMethods; 30 import org.mozilla.gecko.NativeQueue; 31 import org.mozilla.gecko.annotation.WrapForJNI; 32 import org.mozilla.gecko.util.ThreadUtils; 33 34 /** 35 * {@code SessionTextInput} handles text input for {@code GeckoSession} through key events or input 36 * methods. It is typically used to implement certain methods in {@link android.view.View} such as 37 * {@link android.view.View#onCreateInputConnection}, by forwarding such calls to corresponding 38 * methods in {@code SessionTextInput}. 39 * 40 * <p>For full functionality, {@code SessionTextInput} requires a {@link android.view.View} to be 41 * set first through {@link #setView}. When a {@link android.view.View} is not set or set to null, 42 * {@code SessionTextInput} will operate in a reduced functionality mode. See {@link 43 * #onCreateInputConnection} and methods in {@link GeckoSession.TextInputDelegate} for changes in 44 * behavior in this viewless mode. 45 */ 46 public final class SessionTextInput { 47 /* package */ static final String LOGTAG = "GeckoSessionTextInput"; 48 private static final boolean DEBUG = false; 49 50 // Interface to access GeckoInputConnection from SessionTextInput. 51 /* package */ interface InputConnectionClient { 52 View getView(); 53 54 Handler getHandler(Handler defHandler); 55 56 InputConnection onCreateInputConnection(EditorInfo attrs); 57 } 58 59 // Interface to access GeckoEditable from GeckoInputConnection. 60 /* package */ interface EditableClient { 61 // The following value is used by requestCursorUpdates 62 // ONE_SHOT calls updateCompositionRects() after getting current composing 63 // character rects. 64 @Retention(RetentionPolicy.SOURCE) 65 @IntDef({ONE_SHOT, START_MONITOR, END_MONITOR}) 66 /* package */ @interface CursorMonitorMode {} 67 68 @WrapForJNI int ONE_SHOT = 1; 69 // START_MONITOR start the monitor for composing character rects. If is is 70 // updaed, call updateCompositionRects() 71 @WrapForJNI int START_MONITOR = 2; 72 // ENDT_MONITOR stops the monitor for composing character rects. 73 @WrapForJNI int END_MONITOR = 3; 74 75 void sendKeyEvent(@Nullable View view, int action, @NonNull KeyEvent event); 76 77 Editable getEditable(); 78 79 void setBatchMode(boolean isBatchMode); 80 81 Handler setInputConnectionHandler(@NonNull Handler handler); 82 83 void postToInputConnection(@NonNull Runnable runnable); 84 85 void requestCursorUpdates(@CursorMonitorMode int requestMode); 86 87 void insertImage(@NonNull byte[] data, @NonNull String mimeType); 88 } 89 90 // Interface to access GeckoInputConnection from GeckoEditable. 91 /* package */ interface EditableListener { 92 // IME notification type for notifyIME(), corresponding to NotificationToIME enum. 93 @Retention(RetentionPolicy.SOURCE) 94 @IntDef({ 95 NOTIFY_IME_OF_TOKEN, 96 NOTIFY_IME_OPEN_VKB, 97 NOTIFY_IME_REPLY_EVENT, 98 NOTIFY_IME_OF_FOCUS, 99 NOTIFY_IME_OF_BLUR, 100 NOTIFY_IME_TO_COMMIT_COMPOSITION, 101 NOTIFY_IME_TO_CANCEL_COMPOSITION 102 }) 103 /* package */ @interface IMENotificationType {} 104 105 @WrapForJNI int NOTIFY_IME_OF_TOKEN = -3; 106 @WrapForJNI int NOTIFY_IME_OPEN_VKB = -2; 107 @WrapForJNI int NOTIFY_IME_REPLY_EVENT = -1; 108 @WrapForJNI int NOTIFY_IME_OF_FOCUS = 1; 109 @WrapForJNI int NOTIFY_IME_OF_BLUR = 2; 110 @WrapForJNI int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8; 111 @WrapForJNI int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9; 112 113 // IME enabled state for notifyIMEContext(). 114 @Retention(RetentionPolicy.SOURCE) 115 @IntDef({IME_STATE_UNKNOWN, IME_STATE_DISABLED, IME_STATE_ENABLED, IME_STATE_PASSWORD}) 116 /* package */ @interface IMEState {} 117 118 int IME_STATE_UNKNOWN = -1; 119 int IME_STATE_DISABLED = 0; 120 int IME_STATE_ENABLED = 1; 121 int IME_STATE_PASSWORD = 2; 122 123 // Flags for notifyIMEContext(). 124 @Retention(RetentionPolicy.SOURCE) 125 @IntDef( 126 flag = true, 127 value = {IME_FLAG_PRIVATE_BROWSING, IME_FLAG_USER_ACTION, IME_FOCUS_NOT_CHANGED}) 128 /* package */ @interface IMEContextFlags {} 129 130 @WrapForJNI int IME_FLAG_PRIVATE_BROWSING = 1 << 0; 131 @WrapForJNI int IME_FLAG_USER_ACTION = 1 << 1; 132 @WrapForJNI int IME_FOCUS_NOT_CHANGED = 1 << 2; 133 134 void notifyIME(@IMENotificationType int type); 135 136 void notifyIMEContext( 137 @IMEState int state, 138 String typeHint, 139 String modeHint, 140 String actionHint, 141 @IMEContextFlags int flag); 142 143 void onSelectionChange(); 144 145 void onTextChange(); 146 147 void onDiscardComposition(); 148 149 void onDefaultKeyEvent(KeyEvent event); 150 151 void updateCompositionRects(final RectF[] aRects, final RectF aCaretRect); 152 } 153 154 private static final class DefaultDelegate implements GeckoSession.TextInputDelegate { 155 public static final DefaultDelegate INSTANCE = new DefaultDelegate(); 156 157 private InputMethodManager getInputMethodManager(@Nullable final View view) { 158 if (view == null) { 159 return null; 160 } 161 return (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 162 } 163 164 @Override 165 public void restartInput(@NonNull final GeckoSession session, final int reason) { 166 ThreadUtils.assertOnUiThread(); 167 final View view = session.getTextInput().getView(); 168 169 final InputMethodManager imm = getInputMethodManager(view); 170 if (imm == null) { 171 return; 172 } 173 174 // InputMethodManager has internal logic to detect if we are restarting input 175 // in an already focused View, which is the case here because all content text 176 // fields are inside one LayerView. When this happens, InputMethodManager will 177 // tell the input method to soft reset instead of hard reset. Stock latin IME 178 // on Android 4.2+ has a quirk that when it soft resets, it does not clear the 179 // composition. The following workaround tricks the IME into clearing the 180 // composition when soft resetting. 181 if (InputMethods.needsSoftResetWorkaround( 182 InputMethods.getCurrentInputMethod(view.getContext()))) { 183 // Fake a selection change, because the IME clears the composition when 184 // the selection changes, even if soft-resetting. Offsets here must be 185 // different from the previous selection offsets, and -1 seems to be a 186 // reasonable, deterministic value 187 imm.updateSelection(view, -1, -1, -1, -1); 188 } 189 190 try { 191 imm.restartInput(view); 192 } catch (final RuntimeException e) { 193 Log.e(LOGTAG, "Error restarting input", e); 194 } 195 } 196 197 @Override 198 public void showSoftInput(@NonNull final GeckoSession session) { 199 ThreadUtils.assertOnUiThread(); 200 final View view = session.getTextInput().getView(); 201 final InputMethodManager imm = getInputMethodManager(view); 202 if (imm != null) { 203 if (view.hasFocus() && !imm.isActive(view)) { 204 // Marshmallow workaround: The view has focus but it is not the active 205 // view for the input method. (Bug 1211848) 206 view.clearFocus(); 207 view.requestFocus(); 208 } 209 imm.showSoftInput(view, 0); 210 } 211 } 212 213 @Override 214 public void hideSoftInput(@NonNull final GeckoSession session) { 215 ThreadUtils.assertOnUiThread(); 216 final View view = session.getTextInput().getView(); 217 final InputMethodManager imm = getInputMethodManager(view); 218 if (imm != null) { 219 imm.hideSoftInputFromWindow(view.getWindowToken(), 0); 220 } 221 } 222 223 @Override 224 public void updateSelection( 225 @NonNull final GeckoSession session, 226 final int selStart, 227 final int selEnd, 228 final int compositionStart, 229 final int compositionEnd) { 230 ThreadUtils.assertOnUiThread(); 231 final View view = session.getTextInput().getView(); 232 final InputMethodManager imm = getInputMethodManager(view); 233 if (imm != null) { 234 // When composition start and end is -1, 235 // InputMethodManager.updateSelection will remove composition 236 // on most IMEs. If not working, we have to add a workaround 237 // to EditableListener.onDiscardComposition. 238 imm.updateSelection(view, selStart, selEnd, compositionStart, compositionEnd); 239 } 240 } 241 242 @Override 243 public void updateExtractedText( 244 @NonNull final GeckoSession session, 245 @NonNull final ExtractedTextRequest request, 246 @NonNull final ExtractedText text) { 247 ThreadUtils.assertOnUiThread(); 248 final View view = session.getTextInput().getView(); 249 final InputMethodManager imm = getInputMethodManager(view); 250 if (imm != null) { 251 imm.updateExtractedText(view, request.token, text); 252 } 253 } 254 255 @Override 256 public void updateCursorAnchorInfo( 257 @NonNull final GeckoSession session, @NonNull final CursorAnchorInfo info) { 258 ThreadUtils.assertOnUiThread(); 259 final View view = session.getTextInput().getView(); 260 final InputMethodManager imm = getInputMethodManager(view); 261 if (imm != null) { 262 imm.updateCursorAnchorInfo(view, info); 263 } 264 } 265 } 266 267 private final GeckoSession mSession; 268 private final NativeQueue mQueue; 269 private final GeckoEditable mEditable; 270 private InputConnectionClient mInputConnection; 271 private GeckoSession.TextInputDelegate mDelegate; 272 273 /* package */ SessionTextInput( 274 final @NonNull GeckoSession session, final @NonNull NativeQueue queue) { 275 mSession = session; 276 mQueue = queue; 277 mEditable = new GeckoEditable(session); 278 } 279 280 /* package */ void onWindowChanged(final GeckoSession.Window window) { 281 if (mQueue.isReady()) { 282 window.attachEditable(mEditable); 283 } else { 284 mQueue.queueUntilReady(window, "attachEditable", IGeckoEditableParent.class, mEditable); 285 } 286 } 287 288 /** 289 * Get a Handler for the background input method thread. 290 * 291 * @param defHandler Handler returned by the system {@code getHandler} implementation. 292 * @return Handler to return to the system through {@code getHandler}. 293 */ 294 @AnyThread 295 public synchronized @NonNull Handler getHandler(final @NonNull Handler defHandler) { 296 // May be called on any thread. 297 if (mInputConnection != null) { 298 return mInputConnection.getHandler(defHandler); 299 } 300 return defHandler; 301 } 302 303 /** 304 * Get the current {@link android.view.View} for text input. 305 * 306 * @return Current text input View or null if not set. 307 * @see #setView(View) 308 */ 309 @UiThread 310 public @Nullable View getView() { 311 ThreadUtils.assertOnUiThread(); 312 return mInputConnection != null ? mInputConnection.getView() : null; 313 } 314 315 /** 316 * Set the current {@link android.view.View} for text input. The {@link android.view.View} is used 317 * to interact with the system input method manager and to display certain text input UI elements. 318 * See the {@code SessionTextInput} class documentation for information on viewless mode, when the 319 * current {@link android.view.View} is not set or set to null. 320 * 321 * @param view Text input View or null to clear current View. 322 * @see #getView() 323 */ 324 @UiThread 325 public synchronized void setView(final @Nullable View view) { 326 ThreadUtils.assertOnUiThread(); 327 328 if (view == null) { 329 mInputConnection = null; 330 } else if (mInputConnection == null || mInputConnection.getView() != view) { 331 mInputConnection = GeckoInputConnection.create(mSession, view, mEditable); 332 } 333 mEditable.setListener((EditableListener) mInputConnection); 334 } 335 336 /** 337 * Get an {@link android.view.inputmethod.InputConnection} instance. In viewless mode, this method 338 * still fills out the {@link android.view.inputmethod.EditorInfo} object, but the return value 339 * will always be null. 340 * 341 * @param attrs EditorInfo instance to be filled on return. 342 * @return InputConnection instance, or null if there is no active input (or if in viewless mode). 343 */ 344 @AnyThread 345 public synchronized @Nullable InputConnection onCreateInputConnection( 346 final @NonNull EditorInfo attrs) { 347 // May be called on any thread. 348 mEditable.onCreateInputConnection(attrs); 349 350 if (!mQueue.isReady() || mInputConnection == null) { 351 return null; 352 } 353 return mInputConnection.onCreateInputConnection(attrs); 354 } 355 356 /** 357 * Process a KeyEvent as a pre-IME event. 358 * 359 * @param keyCode Key code. 360 * @param event KeyEvent instance. 361 * @return True if the event was handled. 362 */ 363 @UiThread 364 public boolean onKeyPreIme(final int keyCode, final @NonNull KeyEvent event) { 365 ThreadUtils.assertOnUiThread(); 366 return mEditable.onKeyPreIme(getView(), keyCode, event); 367 } 368 369 /** 370 * Process a KeyEvent as a key-down event. 371 * 372 * @param keyCode Key code. 373 * @param event KeyEvent instance. 374 * @return True if the event was handled. 375 */ 376 @UiThread 377 public boolean onKeyDown(final int keyCode, final @NonNull KeyEvent event) { 378 ThreadUtils.assertOnUiThread(); 379 return mEditable.onKeyDown(getView(), keyCode, event); 380 } 381 382 /** 383 * Process a KeyEvent as a key-up event. 384 * 385 * @param keyCode Key code. 386 * @param event KeyEvent instance. 387 * @return True if the event was handled. 388 */ 389 @UiThread 390 public boolean onKeyUp(final int keyCode, final @NonNull KeyEvent event) { 391 ThreadUtils.assertOnUiThread(); 392 return mEditable.onKeyUp(getView(), keyCode, event); 393 } 394 395 /** 396 * Process a KeyEvent as a long-press event. 397 * 398 * @param keyCode Key code. 399 * @param event KeyEvent instance. 400 * @return True if the event was handled. 401 */ 402 @UiThread 403 public boolean onKeyLongPress(final int keyCode, final @NonNull KeyEvent event) { 404 ThreadUtils.assertOnUiThread(); 405 return mEditable.onKeyLongPress(getView(), keyCode, event); 406 } 407 408 /** 409 * Process a KeyEvent as a multiple-press event. 410 * 411 * @param keyCode Key code. 412 * @param repeatCount Key repeat count. 413 * @param event KeyEvent instance. 414 * @return True if the event was handled. 415 */ 416 @UiThread 417 public boolean onKeyMultiple( 418 final int keyCode, final int repeatCount, final @NonNull KeyEvent event) { 419 ThreadUtils.assertOnUiThread(); 420 return mEditable.onKeyMultiple(getView(), keyCode, repeatCount, event); 421 } 422 423 /** 424 * Set the current text input delegate. 425 * 426 * @param delegate TextInputDelegate instance or null to restore to default. 427 */ 428 @UiThread 429 public void setDelegate(@Nullable final GeckoSession.TextInputDelegate delegate) { 430 ThreadUtils.assertOnUiThread(); 431 mDelegate = delegate; 432 } 433 434 /** 435 * Get the current text input delegate. 436 * 437 * @return TextInputDelegate instance or a default instance if no delegate has been set. 438 */ 439 @UiThread 440 public @NonNull GeckoSession.TextInputDelegate getDelegate() { 441 ThreadUtils.assertOnUiThread(); 442 if (mDelegate == null) { 443 mDelegate = DefaultDelegate.INSTANCE; 444 } 445 return mDelegate; 446 } 447 }