Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko.toolbar; |
michael@0 | 7 | |
michael@0 | 8 | import org.mozilla.gecko.R; |
michael@0 | 9 | import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; |
michael@0 | 10 | import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; |
michael@0 | 11 | import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; |
michael@0 | 12 | import org.mozilla.gecko.CustomEditText; |
michael@0 | 13 | import org.mozilla.gecko.CustomEditText.OnKeyPreImeListener; |
michael@0 | 14 | import org.mozilla.gecko.InputMethods; |
michael@0 | 15 | import org.mozilla.gecko.util.GamepadUtils; |
michael@0 | 16 | import org.mozilla.gecko.util.StringUtils; |
michael@0 | 17 | |
michael@0 | 18 | import android.content.Context; |
michael@0 | 19 | import android.graphics.Rect; |
michael@0 | 20 | import android.text.Editable; |
michael@0 | 21 | import android.text.InputType; |
michael@0 | 22 | import android.text.Spanned; |
michael@0 | 23 | import android.text.TextUtils; |
michael@0 | 24 | import android.text.TextWatcher; |
michael@0 | 25 | import android.util.AttributeSet; |
michael@0 | 26 | import android.util.Log; |
michael@0 | 27 | import android.view.KeyEvent; |
michael@0 | 28 | import android.view.View; |
michael@0 | 29 | import android.view.View.OnKeyListener; |
michael@0 | 30 | import android.view.inputmethod.EditorInfo; |
michael@0 | 31 | import android.view.inputmethod.InputMethodManager; |
michael@0 | 32 | |
michael@0 | 33 | /** |
michael@0 | 34 | * {@code ToolbarEditText} is the text entry used when the toolbar |
michael@0 | 35 | * is in edit state. It handles all the necessary input method machinery |
michael@0 | 36 | * as well as the tracking of different text types (empty, search, or url). |
michael@0 | 37 | * It's meant to be owned by {@code ToolbarEditLayout}. |
michael@0 | 38 | */ |
michael@0 | 39 | public class ToolbarEditText extends CustomEditText |
michael@0 | 40 | implements AutocompleteHandler { |
michael@0 | 41 | |
michael@0 | 42 | private static final String LOGTAG = "GeckoToolbarEditText"; |
michael@0 | 43 | |
michael@0 | 44 | // Used to track the current type of content in the |
michael@0 | 45 | // text entry so that ToolbarEditLayout can update its |
michael@0 | 46 | // state accordingly. |
michael@0 | 47 | enum TextType { |
michael@0 | 48 | EMPTY, |
michael@0 | 49 | SEARCH_QUERY, |
michael@0 | 50 | URL |
michael@0 | 51 | } |
michael@0 | 52 | |
michael@0 | 53 | interface OnTextTypeChangeListener { |
michael@0 | 54 | public void onTextTypeChange(ToolbarEditText editText, TextType textType); |
michael@0 | 55 | } |
michael@0 | 56 | |
michael@0 | 57 | private final Context mContext; |
michael@0 | 58 | |
michael@0 | 59 | // Type of the URL bar go/search button |
michael@0 | 60 | private TextType mToolbarTextType; |
michael@0 | 61 | // Type of the keyboard go/search button (cannot be EMPTY) |
michael@0 | 62 | private TextType mKeyboardTextType; |
michael@0 | 63 | |
michael@0 | 64 | private OnCommitListener mCommitListener; |
michael@0 | 65 | private OnDismissListener mDismissListener; |
michael@0 | 66 | private OnFilterListener mFilterListener; |
michael@0 | 67 | private OnTextTypeChangeListener mTextTypeListener; |
michael@0 | 68 | |
michael@0 | 69 | // The previous autocomplete result returned to us |
michael@0 | 70 | private String mAutoCompleteResult = ""; |
michael@0 | 71 | |
michael@0 | 72 | // The user typed part of the autocomplete result |
michael@0 | 73 | private String mAutoCompletePrefix = null; |
michael@0 | 74 | |
michael@0 | 75 | private boolean mDelayRestartInput; |
michael@0 | 76 | |
michael@0 | 77 | public ToolbarEditText(Context context, AttributeSet attrs) { |
michael@0 | 78 | super(context, attrs); |
michael@0 | 79 | mContext = context; |
michael@0 | 80 | |
michael@0 | 81 | mToolbarTextType = TextType.EMPTY; |
michael@0 | 82 | mKeyboardTextType = TextType.URL; |
michael@0 | 83 | } |
michael@0 | 84 | |
michael@0 | 85 | void setOnCommitListener(OnCommitListener listener) { |
michael@0 | 86 | mCommitListener = listener; |
michael@0 | 87 | } |
michael@0 | 88 | |
michael@0 | 89 | void setOnDismissListener(OnDismissListener listener) { |
michael@0 | 90 | mDismissListener = listener; |
michael@0 | 91 | } |
michael@0 | 92 | |
michael@0 | 93 | void setOnFilterListener(OnFilterListener listener) { |
michael@0 | 94 | mFilterListener = listener; |
michael@0 | 95 | } |
michael@0 | 96 | |
michael@0 | 97 | void setOnTextTypeChangeListener(OnTextTypeChangeListener listener) { |
michael@0 | 98 | mTextTypeListener = listener; |
michael@0 | 99 | } |
michael@0 | 100 | |
michael@0 | 101 | @Override |
michael@0 | 102 | public void onAttachedToWindow() { |
michael@0 | 103 | setOnKeyListener(new KeyListener()); |
michael@0 | 104 | setOnKeyPreImeListener(new KeyPreImeListener()); |
michael@0 | 105 | addTextChangedListener(new TextChangeListener()); |
michael@0 | 106 | } |
michael@0 | 107 | |
michael@0 | 108 | @Override |
michael@0 | 109 | public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { |
michael@0 | 110 | super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); |
michael@0 | 111 | |
michael@0 | 112 | if (gainFocus) { |
michael@0 | 113 | resetAutocompleteState(); |
michael@0 | 114 | return; |
michael@0 | 115 | } |
michael@0 | 116 | |
michael@0 | 117 | InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); |
michael@0 | 118 | try { |
michael@0 | 119 | imm.hideSoftInputFromWindow(getWindowToken(), 0); |
michael@0 | 120 | } catch (NullPointerException e) { |
michael@0 | 121 | Log.e(LOGTAG, "InputMethodManagerService, why are you throwing" |
michael@0 | 122 | + " a NullPointerException? See bug 782096", e); |
michael@0 | 123 | } |
michael@0 | 124 | } |
michael@0 | 125 | |
michael@0 | 126 | // Return early if we're backspacing through the string, or |
michael@0 | 127 | // have no autocomplete results |
michael@0 | 128 | @Override |
michael@0 | 129 | public final void onAutocomplete(final String result) { |
michael@0 | 130 | if (!isEnabled()) { |
michael@0 | 131 | return; |
michael@0 | 132 | } |
michael@0 | 133 | |
michael@0 | 134 | final String text = getText().toString(); |
michael@0 | 135 | |
michael@0 | 136 | if (result == null) { |
michael@0 | 137 | mAutoCompleteResult = ""; |
michael@0 | 138 | return; |
michael@0 | 139 | } |
michael@0 | 140 | |
michael@0 | 141 | if (!result.startsWith(text) || text.equals(result)) { |
michael@0 | 142 | return; |
michael@0 | 143 | } |
michael@0 | 144 | |
michael@0 | 145 | mAutoCompleteResult = result; |
michael@0 | 146 | getText().append(result.substring(text.length())); |
michael@0 | 147 | setSelection(text.length(), result.length()); |
michael@0 | 148 | } |
michael@0 | 149 | |
michael@0 | 150 | @Override |
michael@0 | 151 | public void setEnabled(boolean enabled) { |
michael@0 | 152 | super.setEnabled(enabled); |
michael@0 | 153 | updateTextTypeFromText(getText().toString()); |
michael@0 | 154 | } |
michael@0 | 155 | |
michael@0 | 156 | private void resetAutocompleteState() { |
michael@0 | 157 | mAutoCompleteResult = ""; |
michael@0 | 158 | mAutoCompletePrefix = null; |
michael@0 | 159 | } |
michael@0 | 160 | |
michael@0 | 161 | private void updateKeyboardInputType() { |
michael@0 | 162 | // If the user enters a space, then we know they are entering |
michael@0 | 163 | // search terms, not a URL. We can then switch to text mode so, |
michael@0 | 164 | // 1) the IME auto-inserts spaces between words |
michael@0 | 165 | // 2) the IME doesn't reset input keyboard to Latin keyboard. |
michael@0 | 166 | final String text = getText().toString(); |
michael@0 | 167 | final int currentInputType = getInputType(); |
michael@0 | 168 | |
michael@0 | 169 | final int newInputType = StringUtils.isSearchQuery(text, false) |
michael@0 | 170 | ? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode |
michael@0 | 171 | : (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode |
michael@0 | 172 | |
michael@0 | 173 | if (newInputType != currentInputType) { |
michael@0 | 174 | setRawInputType(newInputType); |
michael@0 | 175 | } |
michael@0 | 176 | } |
michael@0 | 177 | |
michael@0 | 178 | private static boolean hasCompositionString(Editable content) { |
michael@0 | 179 | Object[] spans = content.getSpans(0, content.length(), Object.class); |
michael@0 | 180 | |
michael@0 | 181 | if (spans != null) { |
michael@0 | 182 | for (Object span : spans) { |
michael@0 | 183 | if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { |
michael@0 | 184 | // Found composition string. |
michael@0 | 185 | return true; |
michael@0 | 186 | } |
michael@0 | 187 | } |
michael@0 | 188 | } |
michael@0 | 189 | |
michael@0 | 190 | return false; |
michael@0 | 191 | } |
michael@0 | 192 | |
michael@0 | 193 | private void setTextType(TextType textType) { |
michael@0 | 194 | mToolbarTextType = textType; |
michael@0 | 195 | |
michael@0 | 196 | if (textType != TextType.EMPTY) { |
michael@0 | 197 | mKeyboardTextType = textType; |
michael@0 | 198 | } |
michael@0 | 199 | if (mTextTypeListener != null) { |
michael@0 | 200 | mTextTypeListener.onTextTypeChange(this, textType); |
michael@0 | 201 | } |
michael@0 | 202 | } |
michael@0 | 203 | |
michael@0 | 204 | private void updateTextTypeFromText(String text) { |
michael@0 | 205 | if (text.length() == 0) { |
michael@0 | 206 | setTextType(TextType.EMPTY); |
michael@0 | 207 | return; |
michael@0 | 208 | } |
michael@0 | 209 | |
michael@0 | 210 | if (InputMethods.shouldDisableUrlBarUpdate(mContext)) { |
michael@0 | 211 | // Set button type to match the previous keyboard type |
michael@0 | 212 | setTextType(mKeyboardTextType); |
michael@0 | 213 | return; |
michael@0 | 214 | } |
michael@0 | 215 | |
michael@0 | 216 | final int actionBits = getImeOptions() & EditorInfo.IME_MASK_ACTION; |
michael@0 | 217 | |
michael@0 | 218 | final int imeAction; |
michael@0 | 219 | if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) { |
michael@0 | 220 | imeAction = EditorInfo.IME_ACTION_SEARCH; |
michael@0 | 221 | } else { |
michael@0 | 222 | imeAction = EditorInfo.IME_ACTION_GO; |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | InputMethodManager imm = InputMethods.getInputMethodManager(mContext); |
michael@0 | 226 | if (imm == null) { |
michael@0 | 227 | return; |
michael@0 | 228 | } |
michael@0 | 229 | |
michael@0 | 230 | boolean restartInput = false; |
michael@0 | 231 | if (actionBits != imeAction) { |
michael@0 | 232 | int optionBits = getImeOptions() & ~EditorInfo.IME_MASK_ACTION; |
michael@0 | 233 | setImeOptions(optionBits | imeAction); |
michael@0 | 234 | |
michael@0 | 235 | mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) && |
michael@0 | 236 | (InputMethods.shouldDelayUrlBarUpdate(mContext)); |
michael@0 | 237 | |
michael@0 | 238 | if (!mDelayRestartInput) { |
michael@0 | 239 | restartInput = true; |
michael@0 | 240 | } |
michael@0 | 241 | } else if (mDelayRestartInput) { |
michael@0 | 242 | // Only call delayed restartInput when actionBits == imeAction |
michael@0 | 243 | // so if there are two restarts in a row, the first restarts will |
michael@0 | 244 | // be discarded and the second restart will be properly delayed |
michael@0 | 245 | mDelayRestartInput = false; |
michael@0 | 246 | restartInput = true; |
michael@0 | 247 | } |
michael@0 | 248 | |
michael@0 | 249 | if (!restartInput) { |
michael@0 | 250 | // If the text content was previously empty, the toolbar text type |
michael@0 | 251 | // is empty as well. Since the keyboard text type cannot be empty, |
michael@0 | 252 | // the two text types are now inconsistent. Reset the toolbar text |
michael@0 | 253 | // type here to the keyboard text type to ensure consistency. |
michael@0 | 254 | setTextType(mKeyboardTextType); |
michael@0 | 255 | return; |
michael@0 | 256 | } |
michael@0 | 257 | updateKeyboardInputType(); |
michael@0 | 258 | imm.restartInput(ToolbarEditText.this); |
michael@0 | 259 | |
michael@0 | 260 | setTextType(imeAction == EditorInfo.IME_ACTION_GO ? |
michael@0 | 261 | TextType.URL : TextType.SEARCH_QUERY); |
michael@0 | 262 | } |
michael@0 | 263 | |
michael@0 | 264 | private class TextChangeListener implements TextWatcher { |
michael@0 | 265 | @Override |
michael@0 | 266 | public void afterTextChanged(final Editable s) { |
michael@0 | 267 | if (!isEnabled()) { |
michael@0 | 268 | return; |
michael@0 | 269 | } |
michael@0 | 270 | |
michael@0 | 271 | final String text = s.toString(); |
michael@0 | 272 | |
michael@0 | 273 | boolean useHandler = false; |
michael@0 | 274 | boolean reuseAutocomplete = false; |
michael@0 | 275 | |
michael@0 | 276 | if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) { |
michael@0 | 277 | useHandler = true; |
michael@0 | 278 | |
michael@0 | 279 | // If you're hitting backspace (the string is getting smaller |
michael@0 | 280 | // or is unchanged), don't autocomplete. |
michael@0 | 281 | if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) { |
michael@0 | 282 | useHandler = false; |
michael@0 | 283 | } else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) { |
michael@0 | 284 | // If this text already matches our autocomplete text, autocomplete likely |
michael@0 | 285 | // won't change. Just reuse the old autocomplete value. |
michael@0 | 286 | useHandler = false; |
michael@0 | 287 | reuseAutocomplete = true; |
michael@0 | 288 | } |
michael@0 | 289 | } |
michael@0 | 290 | |
michael@0 | 291 | // If this is the autocomplete text being set, don't run the filter. |
michael@0 | 292 | if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) { |
michael@0 | 293 | if (mFilterListener != null) { |
michael@0 | 294 | mFilterListener.onFilter(text, useHandler ? ToolbarEditText.this : null); |
michael@0 | 295 | } |
michael@0 | 296 | |
michael@0 | 297 | mAutoCompletePrefix = text; |
michael@0 | 298 | |
michael@0 | 299 | if (reuseAutocomplete) { |
michael@0 | 300 | onAutocomplete(mAutoCompleteResult); |
michael@0 | 301 | } |
michael@0 | 302 | } |
michael@0 | 303 | |
michael@0 | 304 | // If the edit text has a composition string, don't call updateGoButton(). |
michael@0 | 305 | // That method resets IME and composition state will be broken. |
michael@0 | 306 | if (!hasCompositionString(s) || InputMethods.isGestureKeyboard(mContext)) { |
michael@0 | 307 | updateTextTypeFromText(text); |
michael@0 | 308 | } |
michael@0 | 309 | } |
michael@0 | 310 | |
michael@0 | 311 | @Override |
michael@0 | 312 | public void beforeTextChanged(CharSequence s, int start, int count, |
michael@0 | 313 | int after) { |
michael@0 | 314 | // do nothing |
michael@0 | 315 | } |
michael@0 | 316 | |
michael@0 | 317 | @Override |
michael@0 | 318 | public void onTextChanged(CharSequence s, int start, int before, |
michael@0 | 319 | int count) { |
michael@0 | 320 | // do nothing |
michael@0 | 321 | } |
michael@0 | 322 | } |
michael@0 | 323 | |
michael@0 | 324 | private class KeyPreImeListener implements OnKeyPreImeListener { |
michael@0 | 325 | @Override |
michael@0 | 326 | public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) { |
michael@0 | 327 | // We only want to process one event per tap |
michael@0 | 328 | if (event.getAction() != KeyEvent.ACTION_DOWN) { |
michael@0 | 329 | return false; |
michael@0 | 330 | } |
michael@0 | 331 | |
michael@0 | 332 | if (keyCode == KeyEvent.KEYCODE_ENTER) { |
michael@0 | 333 | // If the edit text has a composition string, don't submit the text yet. |
michael@0 | 334 | // ENTER is needed to commit the composition string. |
michael@0 | 335 | final Editable content = getText(); |
michael@0 | 336 | if (!hasCompositionString(content)) { |
michael@0 | 337 | if (mCommitListener != null) { |
michael@0 | 338 | mCommitListener.onCommit(); |
michael@0 | 339 | } |
michael@0 | 340 | |
michael@0 | 341 | return true; |
michael@0 | 342 | } |
michael@0 | 343 | } |
michael@0 | 344 | |
michael@0 | 345 | if (keyCode == KeyEvent.KEYCODE_BACK) { |
michael@0 | 346 | // Drop the virtual keyboard. |
michael@0 | 347 | clearFocus(); |
michael@0 | 348 | return true; |
michael@0 | 349 | } |
michael@0 | 350 | |
michael@0 | 351 | return false; |
michael@0 | 352 | } |
michael@0 | 353 | } |
michael@0 | 354 | |
michael@0 | 355 | private class KeyListener implements View.OnKeyListener { |
michael@0 | 356 | @Override |
michael@0 | 357 | public boolean onKey(View v, int keyCode, KeyEvent event) { |
michael@0 | 358 | if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) { |
michael@0 | 359 | if (event.getAction() != KeyEvent.ACTION_DOWN) { |
michael@0 | 360 | return true; |
michael@0 | 361 | } |
michael@0 | 362 | |
michael@0 | 363 | if (mCommitListener != null) { |
michael@0 | 364 | mCommitListener.onCommit(); |
michael@0 | 365 | } |
michael@0 | 366 | |
michael@0 | 367 | return true; |
michael@0 | 368 | } else if (GamepadUtils.isBackKey(event)) { |
michael@0 | 369 | if (mDismissListener != null) { |
michael@0 | 370 | mDismissListener.onDismiss(); |
michael@0 | 371 | } |
michael@0 | 372 | |
michael@0 | 373 | return true; |
michael@0 | 374 | } |
michael@0 | 375 | |
michael@0 | 376 | return false; |
michael@0 | 377 | } |
michael@0 | 378 | } |
michael@0 | 379 | } |