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