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