Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / content / public / android / java / src / org / chromium / content / browser / input / ImeAdapter.java
index b4e41b9..9473beb 100644 (file)
@@ -6,15 +6,23 @@ package org.chromium.content.browser.input;
 
 import android.os.Handler;
 import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.UnderlineSpan;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.EditorInfo;
 
-import com.google.common.annotations.VisibleForTesting;
-
 import org.chromium.base.CalledByNative;
 import org.chromium.base.JNINamespace;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.ui.picker.InputDialogContainer;
+
+import java.lang.CharSequence;
 
 /**
  * Adapts and plumbs android IME service onto the chrome text input API.
@@ -39,31 +47,49 @@ import org.chromium.base.JNINamespace;
  */
 @JNINamespace("content")
 public class ImeAdapter {
+
     /**
      * Interface for the delegate that needs to be notified of IME changes.
      */
     public interface ImeAdapterDelegate {
         /**
-         * @param isFinish whether the event is occurring because input is finished.
+         * Called to notify the delegate about synthetic/real key events before sending to renderer.
+         */
+        void onImeEvent();
+
+        /**
+         * Called when a request to hide the keyboard is sent to InputMethodManager.
          */
-        void onImeEvent(boolean isFinish);
-        void onSetFieldValue();
         void onDismissInput();
+
+        /**
+         * @return View that the keyboard should be attached to.
+         */
         View getAttachedView();
+
+        /**
+         * @return Object that should be called for all keyboard show and hide requests.
+         */
         ResultReceiver getNewShowKeyboardReceiver();
     }
 
     private class DelayedDismissInput implements Runnable {
-        private final long mNativeImeAdapter;
+        private long mNativeImeAdapter;
 
         DelayedDismissInput(long nativeImeAdapter) {
             mNativeImeAdapter = nativeImeAdapter;
         }
 
+        // http://crbug.com/413744
+        void detach() {
+            mNativeImeAdapter = 0;
+        }
+
         @Override
         public void run() {
-            attach(mNativeImeAdapter, sTextInputTypeNone, AdapterInputConnection.INVALID_SELECTION,
-                    AdapterInputConnection.INVALID_SELECTION);
+            if (mNativeImeAdapter != 0) {
+                attach(mNativeImeAdapter, sTextInputTypeNone, sTextInputFlagNone);
+            }
             dismissInput(true);
         }
     }
@@ -92,11 +118,20 @@ public class ImeAdapter {
     static int sTextInputTypeTel;
     static int sTextInputTypeNumber;
     static int sTextInputTypeContentEditable;
+    static int sTextInputFlagNone = 0;
+    static int sTextInputFlagAutocompleteOn;
+    static int sTextInputFlagAutocompleteOff;
+    static int sTextInputFlagAutocorrectOn;
+    static int sTextInputFlagAutocorrectOff;
+    static int sTextInputFlagSpellcheckOn;
+    static int sTextInputFlagSpellcheckOff;
     static int sModifierShift;
     static int sModifierAlt;
     static int sModifierCtrl;
     static int sModifierCapsLockOn;
     static int sModifierNumLockOn;
+    static char[] sSingleCharArray = new char[1];
+    static KeyCharacterMap sKeyCharacterMap;
 
     private long mNativeImeAdapterAndroid;
     private InputMethodManagerWrapper mInputMethodManagerWrapper;
@@ -105,8 +140,11 @@ public class ImeAdapter {
     private final Handler mHandler;
     private DelayedDismissInput mDismissInput = null;
     private int mTextInputType;
-    private int mInitialSelectionStart;
-    private int mInitialSelectionEnd;
+    private int mTextInputFlags;
+    private String mLastComposeText;
+
+    @VisibleForTesting
+    int mLastSyntheticKeyCode;
 
     @VisibleForTesting
     boolean mIsShowWithoutHideOutstanding = false;
@@ -126,11 +164,17 @@ public class ImeAdapter {
      * Default factory for AdapterInputConnection classes.
      */
     public static class AdapterInputConnectionFactory {
-        public AdapterInputConnection get(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
-            return new AdapterInputConnection(view, imeAdapter, outAttrs);
+        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
+                Editable editable, EditorInfo outAttrs) {
+            return new AdapterInputConnection(view, imeAdapter, editable, outAttrs);
         }
     }
 
+    /**
+     * Overrides the InputMethodManagerWrapper that ImeAdapter uses to make calls to
+     * InputMethodManager.
+     * @param immw InputMethodManagerWrapper that should be used to call InputMethodManager.
+     */
     @VisibleForTesting
     public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) {
         mInputMethodManagerWrapper = immw;
@@ -151,10 +195,11 @@ public class ImeAdapter {
      */
     void setInputConnection(AdapterInputConnection inputConnection) {
         mInputConnection = inputConnection;
+        mLastComposeText = null;
     }
 
     /**
-     * Should be only used by AdapterInputConnection.
+     * Should be used only by AdapterInputConnection.
      * @return The input type of currently focused element.
      */
     int getTextInputType() {
@@ -162,21 +207,16 @@ public class ImeAdapter {
     }
 
     /**
-     * Should be only used by AdapterInputConnection.
-     * @return The starting index of the initial text selection.
+     * Should be used only by AdapterInputConnection.
+     * @return The input flags of the currently focused element.
      */
-    int getInitialSelectionStart() {
-        return mInitialSelectionStart;
+    int getTextInputFlags() {
+        return mTextInputFlags;
     }
 
     /**
-     * Should be only used by AdapterInputConnection.
-     * @return The ending index of the initial text selection.
+     * @return Constant representing that a focused node is not an input field.
      */
-    int getInitialSelectionEnd() {
-        return mInitialSelectionEnd;
-    }
-
     public static int getTextInputTypeNone() {
         return sTextInputTypeNone;
     }
@@ -201,17 +241,14 @@ public class ImeAdapter {
         return modifiers;
     }
 
-    public boolean isActive() {
-        return mInputConnection != null && mInputConnection.isActive();
-    }
-
-    private boolean isFor(int nativeImeAdapter, int textInputType) {
-        return mNativeImeAdapterAndroid == nativeImeAdapter &&
-               mTextInputType == textInputType;
-    }
-
-    public void attachAndShowIfNeeded(int nativeImeAdapter, int textInputType,
-            int selectionStart, int selectionEnd, boolean showIfNeeded) {
+    /**
+     * Shows or hides the keyboard based on passed parameters.
+     * @param nativeImeAdapter Pointer to the ImeAdapterAndroid object that is sending the update.
+     * @param textInputType Text input type for the currently focused field in renderer.
+     * @param showIfNeeded Whether the keyboard should be shown if it is currently hidden.
+     */
+    public void updateKeyboardVisibility(long nativeImeAdapter, int textInputType,
+            int textInputFlags, boolean showIfNeeded) {
         mHandler.removeCallbacks(mDismissInput);
 
         // If current input type is none and showIfNeeded is false, IME should not be shown
@@ -220,7 +257,7 @@ public class ImeAdapter {
             return;
         }
 
-        if (!isFor(nativeImeAdapter, textInputType)) {
+        if (mNativeImeAdapterAndroid != nativeImeAdapter || mTextInputType != textInputType) {
             // Set a delayed task to perform unfocus. This avoids hiding the keyboard when tabbing
             // through text inputs or when JS rapidly changes focus to another text element.
             if (textInputType == sTextInputTypeNone) {
@@ -229,7 +266,7 @@ public class ImeAdapter {
                 return;
             }
 
-            attach(nativeImeAdapter, textInputType, selectionStart, selectionEnd);
+            attach(nativeImeAdapter, textInputType, textInputFlags);
 
             mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView());
             if (showIfNeeded) {
@@ -240,18 +277,20 @@ public class ImeAdapter {
         }
     }
 
-    public void attach(long nativeImeAdapter, int textInputType, int selectionStart,
-            int selectionEnd) {
+    public void attach(long nativeImeAdapter, int textInputType, int textInputFlags) {
         if (mNativeImeAdapterAndroid != 0) {
             nativeResetImeAdapter(mNativeImeAdapterAndroid);
         }
         mNativeImeAdapterAndroid = nativeImeAdapter;
         mTextInputType = textInputType;
-        mInitialSelectionStart = selectionStart;
-        mInitialSelectionEnd = selectionEnd;
+        mTextInputFlags = textInputFlags;
+        mLastComposeText = null;
         if (nativeImeAdapter != 0) {
             nativeAttachImeAdapter(mNativeImeAdapterAndroid);
         }
+        if (mTextInputType == sTextInputTypeNone) {
+            dismissInput(false);
+        }
     }
 
     /**
@@ -260,13 +299,7 @@ public class ImeAdapter {
      * @param nativeImeAdapter The pointer to the native ImeAdapter object.
      */
     public void attach(long nativeImeAdapter) {
-        if (mNativeImeAdapterAndroid != 0) {
-            nativeResetImeAdapter(mNativeImeAdapterAndroid);
-        }
-        mNativeImeAdapterAndroid = nativeImeAdapter;
-        if (nativeImeAdapter != 0) {
-            nativeAttachImeAdapter(mNativeImeAdapterAndroid);
-        }
+        attach(nativeImeAdapter, sTextInputTypeNone, sTextInputFlagNone);
     }
 
     private void showKeyboard() {
@@ -297,6 +330,13 @@ public class ImeAdapter {
         return isTextInputType(mTextInputType);
     }
 
+    /**
+     * @return true if the selected text is of password.
+     */
+    public boolean isSelectionPassword() {
+        return mTextInputType == sTextInputTypePassword;
+    }
+
     public boolean dispatchKeyEvent(KeyEvent event) {
         return translateAndSendNativeEvents(event);
     }
@@ -309,49 +349,152 @@ public class ImeAdapter {
         else return COMPOSITION_KEY_CODE;
     }
 
+    /**
+     * @return Android KeyEvent for a single unicode character.  Only one KeyEvent is returned
+     * even if the system determined that various modifier keys (like Shift) would also have
+     * been pressed.
+     */
+    private static KeyEvent androidKeyEventForCharacter(char chr) {
+        if (sKeyCharacterMap == null) {
+            sKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+        }
+        sSingleCharArray[0] = chr;
+        // TODO: Evaluate cost of this system call.
+        KeyEvent[] events = sKeyCharacterMap.getEvents(sSingleCharArray);
+        if (events == null) {  // No known key sequence will create that character.
+            return null;
+        }
+
+        for (int i = 0; i < events.length; ++i) {
+            if (events[i].getAction() == KeyEvent.ACTION_DOWN &&
+                    !KeyEvent.isModifierKey(events[i].getKeyCode())) {
+                return events[i];
+            }
+        }
+
+        return null;  // No printing characters were found.
+    }
+
+    @VisibleForTesting
+    public static KeyEvent getTypedKeyEventGuess(String oldtext, String newtext) {
+        // Starting typing a new composition should add only a single character.  Any composition
+        // beginning with text longer than that must come from something other than typing so
+        // return 0.
+        if (oldtext == null) {
+            if (newtext.length() == 1) {
+                return androidKeyEventForCharacter(newtext.charAt(0));
+            } else {
+                return null;
+            }
+        }
+
+        // The content has grown in length: assume the last character is the key that caused it.
+        if (newtext.length() > oldtext.length() && newtext.startsWith(oldtext))
+            return androidKeyEventForCharacter(newtext.charAt(newtext.length() - 1));
+
+        // The content has shrunk in length: assume that backspace was pressed.
+        if (oldtext.length() > newtext.length() && oldtext.startsWith(newtext))
+            return new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
+
+        // The content is unchanged or has undergone a complex change (i.e. not a simple tail
+        // modification) so return an unknown key-code.
+        return null;
+    }
+
     void sendKeyEventWithKeyCode(int keyCode, int flags) {
-        long eventTime = System.currentTimeMillis();
+        long eventTime = SystemClock.uptimeMillis();
+        mLastSyntheticKeyCode = keyCode;
         translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
                 KeyEvent.ACTION_DOWN, keyCode, 0, 0,
                 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                 flags));
-        translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), eventTime,
+        translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
                 KeyEvent.ACTION_UP, keyCode, 0, 0,
                 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                 flags));
     }
 
     // Calls from Java to C++
+    // TODO: Add performance tracing to more complicated functions.
 
-    boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition,
+    boolean checkCompositionQueueAndCallNative(CharSequence text, int newCursorPosition,
             boolean isCommit) {
         if (mNativeImeAdapterAndroid == 0) return false;
+        mViewEmbedder.onImeEvent();
 
-        // Committing an empty string finishes the current composition.
-        boolean isFinish = text.isEmpty();
-        mViewEmbedder.onImeEvent(isFinish);
-        int keyCode = shouldSendKeyEventWithKeyCode(text);
-        long timeStampMs = System.currentTimeMillis();
+        String textStr = text.toString();
+        int keyCode = shouldSendKeyEventWithKeyCode(textStr);
+        long timeStampMs = SystemClock.uptimeMillis();
 
         if (keyCode != COMPOSITION_KEY_CODE) {
             sendKeyEventWithKeyCode(keyCode,
                     KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
         } else {
-            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
-                    timeStampMs, keyCode, 0);
+            KeyEvent keyEvent = getTypedKeyEventGuess(mLastComposeText, textStr);
+            int modifiers = 0;
+            if (keyEvent != null) {
+                keyCode = keyEvent.getKeyCode();
+                modifiers = getModifiers(keyEvent.getMetaState());
+            } else if (!textStr.equals(mLastComposeText)) {
+                keyCode = KeyEvent.KEYCODE_UNKNOWN;
+            } else {
+                keyCode = -1;
+            }
+
+            // If this is a commit with no previous composition, then treat it as a native
+            // KeyDown/KeyUp pair with no composition rather than a synthetic pair with
+            // composition below.
+            if (keyCode > 0 && isCommit && mLastComposeText == null) {
+                mLastSyntheticKeyCode = keyCode;
+                return translateAndSendNativeEvents(keyEvent) &&
+                       translateAndSendNativeEvents(KeyEvent.changeAction(
+                               keyEvent, KeyEvent.ACTION_UP));
+            }
+
+            // When typing, there is no issue sending KeyDown and KeyUp events around the
+            // composition event because those key events do nothing (other than call JS
+            // handlers).  Typing does not cause changes outside of a KeyPress event which
+            // we don't call here.  However, if the key-code is a control key such as
+            // KEYCODE_DEL then there never is an associated KeyPress event and the KeyDown
+            // event itself causes the action.  The net result below is that the Renderer calls
+            // cancelComposition() and then Android starts anew with setComposingRegion().
+            // This stopping and restarting of composition could be a source of problems
+            // with 3rd party keyboards.
+            //
+            // An alternative is to *not* call nativeSetComposingText() in the non-commit case
+            // below.  This avoids the restart of composition described above but fails to send
+            // an update to the composition while in composition which, strictly speaking,
+            // does not match the spec.
+            //
+            // For now, the solution is to endure the restarting of composition and only dive
+            // into the alternate solution should there be problems in the field.  --bcwhite
+
+            if (keyCode >= 0) {
+                nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
+                        timeStampMs, keyCode, modifiers, 0);
+            }
+
             if (isCommit) {
-                nativeCommitText(mNativeImeAdapterAndroid, text);
+                nativeCommitText(mNativeImeAdapterAndroid, textStr);
+                textStr = null;
             } else {
-                nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition);
+                nativeSetComposingText(mNativeImeAdapterAndroid, text, textStr, newCursorPosition);
+            }
+
+            if (keyCode >= 0) {
+                nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
+                        timeStampMs, keyCode, modifiers, 0);
             }
-            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
-                    timeStampMs, keyCode, 0);
+
+            mLastSyntheticKeyCode = keyCode;
         }
 
+        mLastComposeText = textStr;
         return true;
     }
 
     void finishComposingText() {
+        mLastComposeText = null;
         if (mNativeImeAdapterAndroid == 0) return;
         nativeFinishComposingText(mNativeImeAdapterAndroid);
     }
@@ -374,27 +517,42 @@ public class ImeAdapter {
             // event.
             return false;
         }
-        mViewEmbedder.onImeEvent(false);
+        mViewEmbedder.onImeEvent();
         return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
                 getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
-                                event.isSystem(), event.getUnicodeChar());
+                             /*isSystemKey=*/false, event.getUnicodeChar());
     }
 
-    boolean sendSyntheticKeyEvent(
-            int eventType, long timestampMs, int keyCode, int unicodeChar) {
+    boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int modifiers,
+            int unicodeChar) {
         if (mNativeImeAdapterAndroid == 0) return false;
 
         nativeSendSyntheticKeyEvent(
-                mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
+                mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, modifiers, unicodeChar);
         return true;
     }
 
+    /**
+     * Send a request to the native counterpart to delete a given range of characters.
+     * @param beforeLength Number of characters to extend the selection by before the existing
+     *                     selection.
+     * @param afterLength Number of characters to extend the selection by after the existing
+     *                    selection.
+     * @return Whether the native counterpart of ImeAdapter received the call.
+     */
     boolean deleteSurroundingText(int beforeLength, int afterLength) {
+        mViewEmbedder.onImeEvent();
         if (mNativeImeAdapterAndroid == 0) return false;
         nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength);
         return true;
     }
 
+    /**
+     * Send a request to the native counterpart to set the selection to given range.
+     * @param start Selection start index.
+     * @param end Selection end index.
+     * @return Whether the native counterpart of ImeAdapter received the call.
+     */
     boolean setEditableSelectionOffsets(int start, int end) {
         if (mNativeImeAdapterAndroid == 0) return false;
         nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
@@ -402,14 +560,15 @@ public class ImeAdapter {
     }
 
     /**
-     * Send a request to the native counterpart to set compositing region to given indices.
+     * Send a request to the native counterpart to set composing region to given indices.
      * @param start The start of the composition.
      * @param end The end of the composition.
      * @return Whether the native counterpart of ImeAdapter received the call.
      */
-    boolean setComposingRegion(int start, int end) {
+    boolean setComposingRegion(CharSequence text, int start, int end) {
         if (mNativeImeAdapterAndroid == 0) return false;
         nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
+        mLastComposeText = text != null ? text.toString() : null;
         return true;
     }
 
@@ -497,33 +656,74 @@ public class ImeAdapter {
     }
 
     @CalledByNative
+    private static void initializeTextInputFlags(
+            int textInputFlagAutocompleteOn, int textInputFlagAutocompleteOff,
+            int textInputFlagAutocorrectOn, int textInputFlagAutocorrectOff,
+            int textInputFlagSpellcheckOn, int textInputFlagSpellcheckOff) {
+        sTextInputFlagAutocompleteOn = textInputFlagAutocompleteOn;
+        sTextInputFlagAutocompleteOff = textInputFlagAutocompleteOff;
+        sTextInputFlagAutocorrectOn = textInputFlagAutocorrectOn;
+        sTextInputFlagAutocorrectOff = textInputFlagAutocorrectOff;
+        sTextInputFlagSpellcheckOn = textInputFlagSpellcheckOn;
+        sTextInputFlagSpellcheckOff = textInputFlagSpellcheckOff;
+    }
+
+    @CalledByNative
     private void focusedNodeChanged(boolean isEditable) {
         if (mInputConnection != null && isEditable) mInputConnection.restartInput();
     }
 
     @CalledByNative
+    private void populateUnderlinesFromSpans(CharSequence text, long underlines) {
+        if (!(text instanceof SpannableString)) return;
+
+        SpannableString spannableString = ((SpannableString) text);
+        CharacterStyle spans[] =
+                spannableString.getSpans(0, text.length(), CharacterStyle.class);
+        for (CharacterStyle span : spans) {
+            if (span instanceof BackgroundColorSpan) {
+                nativeAppendBackgroundColorSpan(underlines, spannableString.getSpanStart(span),
+                        spannableString.getSpanEnd(span),
+                        ((BackgroundColorSpan) span).getBackgroundColor());
+            } else if (span instanceof UnderlineSpan) {
+                nativeAppendUnderlineSpan(underlines, spannableString.getSpanStart(span),
+                        spannableString.getSpanEnd(span));
+            }
+        }
+    }
+
+    @CalledByNative
     private void cancelComposition() {
         if (mInputConnection != null) mInputConnection.restartInput();
+        mLastComposeText = null;
     }
 
     @CalledByNative
     void detach() {
-        if (mDismissInput != null) mHandler.removeCallbacks(mDismissInput);
+        if (mDismissInput != null) {
+            mHandler.removeCallbacks(mDismissInput);
+            mDismissInput.detach();
+        }
         mNativeImeAdapterAndroid = 0;
         mTextInputType = 0;
     }
 
     private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid,
-            int eventType, long timestampMs, int keyCode, int unicodeChar);
+            int eventType, long timestampMs, int keyCode, int modifiers, int unicodeChar);
 
     private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
             int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
             int unicodeChar);
 
-    private native void nativeSetComposingText(long nativeImeAdapterAndroid, String text,
-            int newCursorPosition);
+    private static native void nativeAppendUnderlineSpan(long underlinePtr, int start, int end);
+
+    private static native void nativeAppendBackgroundColorSpan(long underlinePtr, int start,
+            int end, int backgroundColor);
+
+    private native void nativeSetComposingText(long nativeImeAdapterAndroid, CharSequence text,
+            String textStr, int newCursorPosition);
 
-    private native void nativeCommitText(long nativeImeAdapterAndroid, String text);
+    private native void nativeCommitText(long nativeImeAdapterAndroid, String textStr);
 
     private native void nativeFinishComposingText(long nativeImeAdapterAndroid);