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