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    * &#64;Override
298    * public Handler getHandler() {
299    *     if (Build.VERSION.SDK_INT &gt;= 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