michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.toolbar; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; michael@0: import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; michael@0: import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; michael@0: import org.mozilla.gecko.CustomEditText; michael@0: import org.mozilla.gecko.CustomEditText.OnKeyPreImeListener; michael@0: import org.mozilla.gecko.InputMethods; michael@0: import org.mozilla.gecko.util.GamepadUtils; michael@0: import org.mozilla.gecko.util.StringUtils; michael@0: michael@0: import android.content.Context; michael@0: import android.graphics.Rect; michael@0: import android.text.Editable; michael@0: import android.text.InputType; michael@0: import android.text.Spanned; michael@0: import android.text.TextUtils; michael@0: import android.text.TextWatcher; michael@0: import android.util.AttributeSet; michael@0: import android.util.Log; michael@0: import android.view.KeyEvent; michael@0: import android.view.View; michael@0: import android.view.View.OnKeyListener; michael@0: import android.view.inputmethod.EditorInfo; michael@0: import android.view.inputmethod.InputMethodManager; michael@0: michael@0: /** michael@0: * {@code ToolbarEditText} is the text entry used when the toolbar michael@0: * is in edit state. It handles all the necessary input method machinery michael@0: * as well as the tracking of different text types (empty, search, or url). michael@0: * It's meant to be owned by {@code ToolbarEditLayout}. michael@0: */ michael@0: public class ToolbarEditText extends CustomEditText michael@0: implements AutocompleteHandler { michael@0: michael@0: private static final String LOGTAG = "GeckoToolbarEditText"; michael@0: michael@0: // Used to track the current type of content in the michael@0: // text entry so that ToolbarEditLayout can update its michael@0: // state accordingly. michael@0: enum TextType { michael@0: EMPTY, michael@0: SEARCH_QUERY, michael@0: URL michael@0: } michael@0: michael@0: interface OnTextTypeChangeListener { michael@0: public void onTextTypeChange(ToolbarEditText editText, TextType textType); michael@0: } michael@0: michael@0: private final Context mContext; michael@0: michael@0: // Type of the URL bar go/search button michael@0: private TextType mToolbarTextType; michael@0: // Type of the keyboard go/search button (cannot be EMPTY) michael@0: private TextType mKeyboardTextType; michael@0: michael@0: private OnCommitListener mCommitListener; michael@0: private OnDismissListener mDismissListener; michael@0: private OnFilterListener mFilterListener; michael@0: private OnTextTypeChangeListener mTextTypeListener; michael@0: michael@0: // The previous autocomplete result returned to us michael@0: private String mAutoCompleteResult = ""; michael@0: michael@0: // The user typed part of the autocomplete result michael@0: private String mAutoCompletePrefix = null; michael@0: michael@0: private boolean mDelayRestartInput; michael@0: michael@0: public ToolbarEditText(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: mContext = context; michael@0: michael@0: mToolbarTextType = TextType.EMPTY; michael@0: mKeyboardTextType = TextType.URL; michael@0: } michael@0: michael@0: void setOnCommitListener(OnCommitListener listener) { michael@0: mCommitListener = listener; michael@0: } michael@0: michael@0: void setOnDismissListener(OnDismissListener listener) { michael@0: mDismissListener = listener; michael@0: } michael@0: michael@0: void setOnFilterListener(OnFilterListener listener) { michael@0: mFilterListener = listener; michael@0: } michael@0: michael@0: void setOnTextTypeChangeListener(OnTextTypeChangeListener listener) { michael@0: mTextTypeListener = listener; michael@0: } michael@0: michael@0: @Override michael@0: public void onAttachedToWindow() { michael@0: setOnKeyListener(new KeyListener()); michael@0: setOnKeyPreImeListener(new KeyPreImeListener()); michael@0: addTextChangedListener(new TextChangeListener()); michael@0: } michael@0: michael@0: @Override michael@0: public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { michael@0: super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); michael@0: michael@0: if (gainFocus) { michael@0: resetAutocompleteState(); michael@0: return; michael@0: } michael@0: michael@0: InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); michael@0: try { michael@0: imm.hideSoftInputFromWindow(getWindowToken(), 0); michael@0: } catch (NullPointerException e) { michael@0: Log.e(LOGTAG, "InputMethodManagerService, why are you throwing" michael@0: + " a NullPointerException? See bug 782096", e); michael@0: } michael@0: } michael@0: michael@0: // Return early if we're backspacing through the string, or michael@0: // have no autocomplete results michael@0: @Override michael@0: public final void onAutocomplete(final String result) { michael@0: if (!isEnabled()) { michael@0: return; michael@0: } michael@0: michael@0: final String text = getText().toString(); michael@0: michael@0: if (result == null) { michael@0: mAutoCompleteResult = ""; michael@0: return; michael@0: } michael@0: michael@0: if (!result.startsWith(text) || text.equals(result)) { michael@0: return; michael@0: } michael@0: michael@0: mAutoCompleteResult = result; michael@0: getText().append(result.substring(text.length())); michael@0: setSelection(text.length(), result.length()); michael@0: } michael@0: michael@0: @Override michael@0: public void setEnabled(boolean enabled) { michael@0: super.setEnabled(enabled); michael@0: updateTextTypeFromText(getText().toString()); michael@0: } michael@0: michael@0: private void resetAutocompleteState() { michael@0: mAutoCompleteResult = ""; michael@0: mAutoCompletePrefix = null; michael@0: } michael@0: michael@0: private void updateKeyboardInputType() { michael@0: // If the user enters a space, then we know they are entering michael@0: // search terms, not a URL. We can then switch to text mode so, michael@0: // 1) the IME auto-inserts spaces between words michael@0: // 2) the IME doesn't reset input keyboard to Latin keyboard. michael@0: final String text = getText().toString(); michael@0: final int currentInputType = getInputType(); michael@0: michael@0: final int newInputType = StringUtils.isSearchQuery(text, false) michael@0: ? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode michael@0: : (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode michael@0: michael@0: if (newInputType != currentInputType) { michael@0: setRawInputType(newInputType); michael@0: } michael@0: } michael@0: michael@0: private static boolean hasCompositionString(Editable content) { michael@0: Object[] spans = content.getSpans(0, content.length(), Object.class); michael@0: michael@0: if (spans != null) { michael@0: for (Object span : spans) { michael@0: if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { michael@0: // Found composition string. michael@0: return true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: private void setTextType(TextType textType) { michael@0: mToolbarTextType = textType; michael@0: michael@0: if (textType != TextType.EMPTY) { michael@0: mKeyboardTextType = textType; michael@0: } michael@0: if (mTextTypeListener != null) { michael@0: mTextTypeListener.onTextTypeChange(this, textType); michael@0: } michael@0: } michael@0: michael@0: private void updateTextTypeFromText(String text) { michael@0: if (text.length() == 0) { michael@0: setTextType(TextType.EMPTY); michael@0: return; michael@0: } michael@0: michael@0: if (InputMethods.shouldDisableUrlBarUpdate(mContext)) { michael@0: // Set button type to match the previous keyboard type michael@0: setTextType(mKeyboardTextType); michael@0: return; michael@0: } michael@0: michael@0: final int actionBits = getImeOptions() & EditorInfo.IME_MASK_ACTION; michael@0: michael@0: final int imeAction; michael@0: if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) { michael@0: imeAction = EditorInfo.IME_ACTION_SEARCH; michael@0: } else { michael@0: imeAction = EditorInfo.IME_ACTION_GO; michael@0: } michael@0: michael@0: InputMethodManager imm = InputMethods.getInputMethodManager(mContext); michael@0: if (imm == null) { michael@0: return; michael@0: } michael@0: michael@0: boolean restartInput = false; michael@0: if (actionBits != imeAction) { michael@0: int optionBits = getImeOptions() & ~EditorInfo.IME_MASK_ACTION; michael@0: setImeOptions(optionBits | imeAction); michael@0: michael@0: mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) && michael@0: (InputMethods.shouldDelayUrlBarUpdate(mContext)); michael@0: michael@0: if (!mDelayRestartInput) { michael@0: restartInput = true; michael@0: } michael@0: } else if (mDelayRestartInput) { michael@0: // Only call delayed restartInput when actionBits == imeAction michael@0: // so if there are two restarts in a row, the first restarts will michael@0: // be discarded and the second restart will be properly delayed michael@0: mDelayRestartInput = false; michael@0: restartInput = true; michael@0: } michael@0: michael@0: if (!restartInput) { michael@0: // If the text content was previously empty, the toolbar text type michael@0: // is empty as well. Since the keyboard text type cannot be empty, michael@0: // the two text types are now inconsistent. Reset the toolbar text michael@0: // type here to the keyboard text type to ensure consistency. michael@0: setTextType(mKeyboardTextType); michael@0: return; michael@0: } michael@0: updateKeyboardInputType(); michael@0: imm.restartInput(ToolbarEditText.this); michael@0: michael@0: setTextType(imeAction == EditorInfo.IME_ACTION_GO ? michael@0: TextType.URL : TextType.SEARCH_QUERY); michael@0: } michael@0: michael@0: private class TextChangeListener implements TextWatcher { michael@0: @Override michael@0: public void afterTextChanged(final Editable s) { michael@0: if (!isEnabled()) { michael@0: return; michael@0: } michael@0: michael@0: final String text = s.toString(); michael@0: michael@0: boolean useHandler = false; michael@0: boolean reuseAutocomplete = false; michael@0: michael@0: if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) { michael@0: useHandler = true; michael@0: michael@0: // If you're hitting backspace (the string is getting smaller michael@0: // or is unchanged), don't autocomplete. michael@0: if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) { michael@0: useHandler = false; michael@0: } else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) { michael@0: // If this text already matches our autocomplete text, autocomplete likely michael@0: // won't change. Just reuse the old autocomplete value. michael@0: useHandler = false; michael@0: reuseAutocomplete = true; michael@0: } michael@0: } michael@0: michael@0: // If this is the autocomplete text being set, don't run the filter. michael@0: if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) { michael@0: if (mFilterListener != null) { michael@0: mFilterListener.onFilter(text, useHandler ? ToolbarEditText.this : null); michael@0: } michael@0: michael@0: mAutoCompletePrefix = text; michael@0: michael@0: if (reuseAutocomplete) { michael@0: onAutocomplete(mAutoCompleteResult); michael@0: } michael@0: } michael@0: michael@0: // If the edit text has a composition string, don't call updateGoButton(). michael@0: // That method resets IME and composition state will be broken. michael@0: if (!hasCompositionString(s) || InputMethods.isGestureKeyboard(mContext)) { michael@0: updateTextTypeFromText(text); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void beforeTextChanged(CharSequence s, int start, int count, michael@0: int after) { michael@0: // do nothing michael@0: } michael@0: michael@0: @Override michael@0: public void onTextChanged(CharSequence s, int start, int before, michael@0: int count) { michael@0: // do nothing michael@0: } michael@0: } michael@0: michael@0: private class KeyPreImeListener implements OnKeyPreImeListener { michael@0: @Override michael@0: public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) { michael@0: // We only want to process one event per tap michael@0: if (event.getAction() != KeyEvent.ACTION_DOWN) { michael@0: return false; michael@0: } michael@0: michael@0: if (keyCode == KeyEvent.KEYCODE_ENTER) { michael@0: // If the edit text has a composition string, don't submit the text yet. michael@0: // ENTER is needed to commit the composition string. michael@0: final Editable content = getText(); michael@0: if (!hasCompositionString(content)) { michael@0: if (mCommitListener != null) { michael@0: mCommitListener.onCommit(); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: if (keyCode == KeyEvent.KEYCODE_BACK) { michael@0: // Drop the virtual keyboard. michael@0: clearFocus(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: private class KeyListener implements View.OnKeyListener { michael@0: @Override michael@0: public boolean onKey(View v, int keyCode, KeyEvent event) { michael@0: if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) { michael@0: if (event.getAction() != KeyEvent.ACTION_DOWN) { michael@0: return true; michael@0: } michael@0: michael@0: if (mCommitListener != null) { michael@0: mCommitListener.onCommit(); michael@0: } michael@0: michael@0: return true; michael@0: } else if (GamepadUtils.isBackKey(event)) { michael@0: if (mDismissListener != null) { michael@0: mDismissListener.onDismiss(); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: } michael@0: }