diff options
25 files changed, 4190 insertions, 5010 deletions
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index b70875060b30..d0c87c62487b 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -338,7 +338,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable en = tbend; if (getSpanStart(spans[i]) < 0) { - setSpan(false, spans[i], + setSpan(true, spans[i], st - tbstart + start, en - tbstart + start, sp.getSpanFlags(spans[i])); @@ -579,8 +579,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanEnds[i] = end; mSpanFlags[i] = flags; - if (send) - sendSpanChanged(what, ostart, oend, nstart, nend); + if (send) sendSpanChanged(what, ostart, oend, nstart, nend); return; } @@ -610,8 +609,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanFlags[mSpanCount] = flags; mSpanCount++; - if (send) - sendSpanAdded(what, nstart, nend); + if (send) sendSpanAdded(what, nstart, nend); } /** diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java new file mode 100644 index 000000000000..880dc345dcdc --- /dev/null +++ b/core/java/android/widget/Editor.java @@ -0,0 +1,3750 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.R; +import android.content.ClipData; +import android.content.ClipData.Item; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.ExtractEditText; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.provider.Settings; +import android.text.DynamicLayout; +import android.text.Editable; +import android.text.InputType; +import android.text.Layout; +import android.text.ParcelableSpan; +import android.text.Selection; +import android.text.SpanWatcher; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.StaticLayout; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.KeyListener; +import android.text.method.MetaKeyKeyListener; +import android.text.method.MovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.text.method.WordIterator; +import android.text.style.EasyEditSpan; +import android.text.style.SuggestionRangeSpan; +import android.text.style.SuggestionSpan; +import android.text.style.TextAppearanceSpan; +import android.text.style.URLSpan; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.ActionMode; +import android.view.ActionMode.Callback; +import android.view.DisplayList; +import android.view.DragEvent; +import android.view.Gravity; +import android.view.HardwareCanvas; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.View.DragShadowBuilder; +import android.view.View.OnClickListener; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.TextView.Drawables; +import android.widget.TextView.OnEditorActionListener; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.widget.EditableInputConnection; + +import java.text.BreakIterator; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; + +/** + * Helper class used by TextView to handle editable text views. + * + * @hide + */ +public class Editor { + static final int BLINK = 500; + private static final float[] TEMP_POSITION = new float[2]; + private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; + + // Cursor Controllers. + InsertionPointCursorController mInsertionPointCursorController; + SelectionModifierCursorController mSelectionModifierCursorController; + ActionMode mSelectionActionMode; + boolean mInsertionControllerEnabled; + boolean mSelectionControllerEnabled; + + // Used to highlight a word when it is corrected by the IME + CorrectionHighlighter mCorrectionHighlighter; + + InputContentType mInputContentType; + InputMethodState mInputMethodState; + + DisplayList[] mTextDisplayLists; + + boolean mFrozenWithFocus; + boolean mSelectionMoved; + boolean mTouchFocusSelected; + + KeyListener mKeyListener; + int mInputType = EditorInfo.TYPE_NULL; + + boolean mDiscardNextActionUp; + boolean mIgnoreActionUpEvent; + + long mShowCursor; + Blink mBlink; + + boolean mCursorVisible = true; + boolean mSelectAllOnFocus; + boolean mTextIsSelectable; + + CharSequence mError; + boolean mErrorWasChanged; + ErrorPopup mErrorPopup; + /** + * This flag is set if the TextView tries to display an error before it + * is attached to the window (so its position is still unknown). + * It causes the error to be shown later, when onAttachedToWindow() + * is called. + */ + boolean mShowErrorAfterAttach; + + boolean mInBatchEditControllers; + + SuggestionsPopupWindow mSuggestionsPopupWindow; + SuggestionRangeSpan mSuggestionRangeSpan; + Runnable mShowSuggestionRunnable; + + final Drawable[] mCursorDrawable = new Drawable[2]; + int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split) + + private Drawable mSelectHandleLeft; + private Drawable mSelectHandleRight; + private Drawable mSelectHandleCenter; + + // Global listener that detects changes in the global position of the TextView + private PositionListener mPositionListener; + + float mLastDownPositionX, mLastDownPositionY; + Callback mCustomSelectionActionModeCallback; + + // Set when this TextView gained focus with some text selected. Will start selection mode. + boolean mCreatedWithASelection; + + private EasyEditSpanController mEasyEditSpanController; + + WordIterator mWordIterator; + SpellChecker mSpellChecker; + + private Rect mTempRect; + + private TextView mTextView; + + Editor(TextView textView) { + mTextView = textView; + mEasyEditSpanController = new EasyEditSpanController(); + mTextView.addTextChangedListener(mEasyEditSpanController); + } + + void onAttachedToWindow() { + if (mShowErrorAfterAttach) { + showError(); + mShowErrorAfterAttach = false; + } + + final ViewTreeObserver observer = mTextView.getViewTreeObserver(); + // No need to create the controller. + // The get method will add the listener on controller creation. + if (mInsertionPointCursorController != null) { + observer.addOnTouchModeChangeListener(mInsertionPointCursorController); + } + if (mSelectionModifierCursorController != null) { + observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); + } + updateSpellCheckSpans(0, mTextView.getText().length(), + true /* create the spell checker if needed */); + } + + void onDetachedFromWindow() { + if (mError != null) { + hideError(); + } + + if (mBlink != null) { + mBlink.removeCallbacks(mBlink); + } + + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.onDetached(); + } + + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.onDetached(); + } + + if (mShowSuggestionRunnable != null) { + mTextView.removeCallbacks(mShowSuggestionRunnable); + } + + invalidateTextDisplayList(); + + if (mSpellChecker != null) { + mSpellChecker.closeSession(); + // Forces the creation of a new SpellChecker next time this window is created. + // Will handle the cases where the settings has been changed in the meantime. + mSpellChecker = null; + } + + hideControllers(); + } + + private void showError() { + if (mTextView.getWindowToken() == null) { + mShowErrorAfterAttach = true; + return; + } + + if (mErrorPopup == null) { + LayoutInflater inflater = LayoutInflater.from(mTextView.getContext()); + final TextView err = (TextView) inflater.inflate( + com.android.internal.R.layout.textview_hint, null); + + final float scale = mTextView.getResources().getDisplayMetrics().density; + mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f)); + mErrorPopup.setFocusable(false); + // The user is entering text, so the input method is needed. We + // don't want the popup to be displayed on top of it. + mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + } + + TextView tv = (TextView) mErrorPopup.getContentView(); + chooseSize(mErrorPopup, mError, tv); + tv.setText(mError); + + mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY()); + mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor()); + } + + public void setError(CharSequence error, Drawable icon) { + mError = TextUtils.stringOrSpannedString(error); + mErrorWasChanged = true; + final Drawables dr = mTextView.mDrawables; + if (dr != null) { + switch (mTextView.getResolvedLayoutDirection()) { + default: + case View.LAYOUT_DIRECTION_LTR: + mTextView.setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, + dr.mDrawableBottom); + break; + case View.LAYOUT_DIRECTION_RTL: + mTextView.setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight, + dr.mDrawableBottom); + break; + } + } else { + mTextView.setCompoundDrawables(null, null, icon, null); + } + + if (mError == null) { + if (mErrorPopup != null) { + if (mErrorPopup.isShowing()) { + mErrorPopup.dismiss(); + } + + mErrorPopup = null; + } + } else { + if (mTextView.isFocused()) { + showError(); + } + } + } + + private void hideError() { + if (mErrorPopup != null) { + if (mErrorPopup.isShowing()) { + mErrorPopup.dismiss(); + } + } + + mShowErrorAfterAttach = false; + } + + /** + * Returns the Y offset to make the pointy top of the error point + * at the middle of the error icon. + */ + private int getErrorX() { + /* + * The "25" is the distance between the point and the right edge + * of the background + */ + final float scale = mTextView.getResources().getDisplayMetrics().density; + + final Drawables dr = mTextView.mDrawables; + return mTextView.getWidth() - mErrorPopup.getWidth() - mTextView.getPaddingRight() - + (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); + } + + /** + * Returns the Y offset to make the pointy top of the error point + * at the bottom of the error icon. + */ + private int getErrorY() { + /* + * Compound, not extended, because the icon is not clipped + * if the text height is smaller. + */ + final int compoundPaddingTop = mTextView.getCompoundPaddingTop(); + int vspace = mTextView.getBottom() - mTextView.getTop() - + mTextView.getCompoundPaddingBottom() - compoundPaddingTop; + + final Drawables dr = mTextView.mDrawables; + int icontop = compoundPaddingTop + + (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; + + /* + * The "2" is the distance between the point and the top edge + * of the background. + */ + final float scale = mTextView.getResources().getDisplayMetrics().density; + return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - mTextView.getHeight() - + (int) (2 * scale + 0.5f); + } + + void createInputContentTypeIfNeeded() { + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + } + + void createInputMethodStateIfNeeded() { + if (mInputMethodState == null) { + mInputMethodState = new InputMethodState(); + } + } + + boolean isCursorVisible() { + // The default value is true, even when there is no associated Editor + return mCursorVisible && mTextView.isTextEditable(); + } + + void prepareCursorControllers() { + boolean windowSupportsHandles = false; + + ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams(); + if (params instanceof WindowManager.LayoutParams) { + WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params; + windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW + || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW; + } + + boolean enabled = windowSupportsHandles && mTextView.getLayout() != null; + mInsertionControllerEnabled = enabled && isCursorVisible(); + mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected(); + + if (!mInsertionControllerEnabled) { + hideInsertionPointCursorController(); + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.onDetached(); + mInsertionPointCursorController = null; + } + } + + if (!mSelectionControllerEnabled) { + stopSelectionActionMode(); + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.onDetached(); + mSelectionModifierCursorController = null; + } + } + } + + private void hideInsertionPointCursorController() { + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.hide(); + } + } + + /** + * Hides the insertion controller and stops text selection mode, hiding the selection controller + */ + void hideControllers() { + hideCursorControllers(); + hideSpanControllers(); + } + + private void hideSpanControllers() { + if (mEasyEditSpanController != null) { + mEasyEditSpanController.hide(); + } + } + + private void hideCursorControllers() { + if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) { + // Should be done before hide insertion point controller since it triggers a show of it + mSuggestionsPopupWindow.hide(); + } + hideInsertionPointCursorController(); + stopSelectionActionMode(); + } + + /** + * Create new SpellCheckSpans on the modified region. + */ + private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) { + if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() && + !(mTextView instanceof ExtractEditText)) { + if (mSpellChecker == null && createSpellChecker) { + mSpellChecker = new SpellChecker(mTextView); + } + if (mSpellChecker != null) { + mSpellChecker.spellCheck(start, end); + } + } + } + + void onScreenStateChanged(int screenState) { + switch (screenState) { + case View.SCREEN_STATE_ON: + resumeBlink(); + break; + case View.SCREEN_STATE_OFF: + suspendBlink(); + break; + } + } + + private void suspendBlink() { + if (mBlink != null) { + mBlink.cancel(); + } + } + + private void resumeBlink() { + if (mBlink != null) { + mBlink.uncancel(); + makeBlink(); + } + } + + void adjustInputType(boolean password, boolean passwordInputType, + boolean webPasswordInputType, boolean numberPasswordInputType) { + // mInputType has been set from inputType, possibly modified by mInputMethod. + // Specialize mInputType to [web]password if we have a text class and the original input + // type was a password. + if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { + if (password || passwordInputType) { + mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) + | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; + } + if (webPasswordInputType) { + mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) + | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; + } + } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) { + if (numberPasswordInputType) { + mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) + | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD; + } + } + } + + private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { + int wid = tv.getPaddingLeft() + tv.getPaddingRight(); + int ht = tv.getPaddingTop() + tv.getPaddingBottom(); + + int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.textview_error_popup_default_width); + Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels, + Layout.Alignment.ALIGN_NORMAL, 1, 0, true); + float max = 0; + for (int i = 0; i < l.getLineCount(); i++) { + max = Math.max(max, l.getLineWidth(i)); + } + + /* + * Now set the popup size to be big enough for the text plus the border capped + * to DEFAULT_MAX_POPUP_WIDTH + */ + pop.setWidth(wid + (int) Math.ceil(max)); + pop.setHeight(ht + l.getHeight()); + } + + void setFrame() { + if (mErrorPopup != null) { + TextView tv = (TextView) mErrorPopup.getContentView(); + chooseSize(mErrorPopup, mError, tv); + mErrorPopup.update(mTextView, getErrorX(), getErrorY(), + mErrorPopup.getWidth(), mErrorPopup.getHeight()); + } + } + + /** + * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state + * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have + * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient. + */ + private boolean canSelectText() { + return hasSelectionController() && mTextView.getText().length() != 0; + } + + /** + * It would be better to rely on the input type for everything. A password inputType should have + * a password transformation. We should hence use isPasswordInputType instead of this method. + * + * We should: + * - Call setInputType in setKeyListener instead of changing the input type directly (which + * would install the correct transformation). + * - Refuse the installation of a non-password transformation in setTransformation if the input + * type is password. + * + * However, this is like this for legacy reasons and we cannot break existing apps. This method + * is useful since it matches what the user can see (obfuscated text or not). + * + * @return true if the current transformation method is of the password type. + */ + private boolean hasPasswordTransformationMethod() { + return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod; + } + + /** + * Adjusts selection to the word under last touch offset. + * Return true if the operation was successfully performed. + */ + private boolean selectCurrentWord() { + if (!canSelectText()) { + return false; + } + + if (hasPasswordTransformationMethod()) { + // Always select all on a password field. + // Cut/copy menu entries are not available for passwords, but being able to select all + // is however useful to delete or paste to replace the entire content. + return mTextView.selectAllText(); + } + + int inputType = mTextView.getInputType(); + int klass = inputType & InputType.TYPE_MASK_CLASS; + int variation = inputType & InputType.TYPE_MASK_VARIATION; + + // Specific text field types: select the entire text for these + if (klass == InputType.TYPE_CLASS_NUMBER || + klass == InputType.TYPE_CLASS_PHONE || + klass == InputType.TYPE_CLASS_DATETIME || + variation == InputType.TYPE_TEXT_VARIATION_URI || + variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || + variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || + variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + return mTextView.selectAllText(); + } + + long lastTouchOffsets = getLastTouchOffsets(); + final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets); + final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets); + + // Safety check in case standard touch event handling has been bypassed + if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false; + if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false; + + int selectionStart, selectionEnd; + + // If a URLSpan (web address, email, phone...) is found at that position, select it. + URLSpan[] urlSpans = ((Spanned) mTextView.getText()). + getSpans(minOffset, maxOffset, URLSpan.class); + if (urlSpans.length >= 1) { + URLSpan urlSpan = urlSpans[0]; + selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan); + selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan); + } else { + final WordIterator wordIterator = getWordIterator(); + wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset); + + selectionStart = wordIterator.getBeginning(minOffset); + selectionEnd = wordIterator.getEnd(maxOffset); + + if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE || + selectionStart == selectionEnd) { + // Possible when the word iterator does not properly handle the text's language + long range = getCharRange(minOffset); + selectionStart = TextUtils.unpackRangeStartFromLong(range); + selectionEnd = TextUtils.unpackRangeEndFromLong(range); + } + } + + Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); + return selectionEnd > selectionStart; + } + + void onLocaleChanged() { + // Will be re-created on demand in getWordIterator with the proper new locale + mWordIterator = null; + } + + /** + * @hide + */ + public WordIterator getWordIterator() { + if (mWordIterator == null) { + mWordIterator = new WordIterator(mTextView.getTextServicesLocale()); + } + return mWordIterator; + } + + private long getCharRange(int offset) { + final int textLength = mTextView.getText().length(); + if (offset + 1 < textLength) { + final char currentChar = mTextView.getText().charAt(offset); + final char nextChar = mTextView.getText().charAt(offset + 1); + if (Character.isSurrogatePair(currentChar, nextChar)) { + return TextUtils.packRangeInLong(offset, offset + 2); + } + } + if (offset < textLength) { + return TextUtils.packRangeInLong(offset, offset + 1); + } + if (offset - 2 >= 0) { + final char previousChar = mTextView.getText().charAt(offset - 1); + final char previousPreviousChar = mTextView.getText().charAt(offset - 2); + if (Character.isSurrogatePair(previousPreviousChar, previousChar)) { + return TextUtils.packRangeInLong(offset - 2, offset); + } + } + if (offset - 1 >= 0) { + return TextUtils.packRangeInLong(offset - 1, offset); + } + return TextUtils.packRangeInLong(offset, offset); + } + + private boolean touchPositionIsInSelection() { + int selectionStart = mTextView.getSelectionStart(); + int selectionEnd = mTextView.getSelectionEnd(); + + if (selectionStart == selectionEnd) { + return false; + } + + if (selectionStart > selectionEnd) { + int tmp = selectionStart; + selectionStart = selectionEnd; + selectionEnd = tmp; + Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); + } + + SelectionModifierCursorController selectionController = getSelectionController(); + int minOffset = selectionController.getMinTouchOffset(); + int maxOffset = selectionController.getMaxTouchOffset(); + + return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); + } + + private PositionListener getPositionListener() { + if (mPositionListener == null) { + mPositionListener = new PositionListener(); + } + return mPositionListener; + } + + private interface TextViewPositionListener { + public void updatePosition(int parentPositionX, int parentPositionY, + boolean parentPositionChanged, boolean parentScrolled); + } + + private boolean isPositionVisible(int positionX, int positionY) { + synchronized (TEMP_POSITION) { + final float[] position = TEMP_POSITION; + position[0] = positionX; + position[1] = positionY; + View view = mTextView; + + while (view != null) { + if (view != mTextView) { + // Local scroll is already taken into account in positionX/Y + position[0] -= view.getScrollX(); + position[1] -= view.getScrollY(); + } + + if (position[0] < 0 || position[1] < 0 || + position[0] > view.getWidth() || position[1] > view.getHeight()) { + return false; + } + + if (!view.getMatrix().isIdentity()) { + view.getMatrix().mapPoints(position); + } + + position[0] += view.getLeft(); + position[1] += view.getTop(); + + final ViewParent parent = view.getParent(); + if (parent instanceof View) { + view = (View) parent; + } else { + // We've reached the ViewRoot, stop iterating + view = null; + } + } + } + + // We've been able to walk up the view hierarchy and the position was never clipped + return true; + } + + private boolean isOffsetVisible(int offset) { + Layout layout = mTextView.getLayout(); + final int line = layout.getLineForOffset(offset); + final int lineBottom = layout.getLineBottom(line); + final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset); + return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(), + lineBottom + mTextView.viewportToContentVerticalOffset()); + } + + /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed + * in the view. Returns false when the position is in the empty space of left/right of text. + */ + private boolean isPositionOnText(float x, float y) { + Layout layout = mTextView.getLayout(); + if (layout == null) return false; + + final int line = mTextView.getLineAtCoordinate(y); + x = mTextView.convertToLocalHorizontalCoordinate(x); + + if (x < layout.getLineLeft(line)) return false; + if (x > layout.getLineRight(line)) return false; + return true; + } + + public boolean performLongClick(boolean handled) { + // Long press in empty space moves cursor and shows the Paste affordance if available. + if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) && + mInsertionControllerEnabled) { + final int offset = mTextView.getOffsetForPosition(mLastDownPositionX, + mLastDownPositionY); + stopSelectionActionMode(); + Selection.setSelection((Spannable) mTextView.getText(), offset); + getInsertionController().showWithActionPopup(); + handled = true; + } + + if (!handled && mSelectionActionMode != null) { + if (touchPositionIsInSelection()) { + // Start a drag + final int start = mTextView.getSelectionStart(); + final int end = mTextView.getSelectionEnd(); + CharSequence selectedText = mTextView.getTransformedText(start, end); + ClipData data = ClipData.newPlainText(null, selectedText); + DragLocalState localState = new DragLocalState(mTextView, start, end); + mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); + stopSelectionActionMode(); + } else { + getSelectionController().hide(); + selectCurrentWord(); + getSelectionController().show(); + } + handled = true; + } + + // Start a new selection + if (!handled) { + handled = startSelectionActionMode(); + } + + return handled; + } + + private long getLastTouchOffsets() { + SelectionModifierCursorController selectionController = getSelectionController(); + final int minOffset = selectionController.getMinTouchOffset(); + final int maxOffset = selectionController.getMaxTouchOffset(); + return TextUtils.packRangeInLong(minOffset, maxOffset); + } + + void onFocusChanged(boolean focused, int direction) { + mShowCursor = SystemClock.uptimeMillis(); + ensureEndedBatchEdit(); + + if (focused) { + int selStart = mTextView.getSelectionStart(); + int selEnd = mTextView.getSelectionEnd(); + + // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection + // mode for these, unless there was a specific selection already started. + final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 && + selEnd == mTextView.getText().length(); + + mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() && + !isFocusHighlighted; + + if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { + // If a tap was used to give focus to that view, move cursor at tap position. + // Has to be done before onTakeFocus, which can be overloaded. + final int lastTapPosition = getLastTapPosition(); + if (lastTapPosition >= 0) { + Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition); + } + + // Note this may have to be moved out of the Editor class + MovementMethod mMovement = mTextView.getMovementMethod(); + if (mMovement != null) { + mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction); + } + + // The DecorView does not have focus when the 'Done' ExtractEditText button is + // pressed. Since it is the ViewAncestor's mView, it requests focus before + // ExtractEditText clears focus, which gives focus to the ExtractEditText. + // This special case ensure that we keep current selection in that case. + // It would be better to know why the DecorView does not have focus at that time. + if (((mTextView instanceof ExtractEditText) || mSelectionMoved) && + selStart >= 0 && selEnd >= 0) { + /* + * Someone intentionally set the selection, so let them + * do whatever it is that they wanted to do instead of + * the default on-focus behavior. We reset the selection + * here instead of just skipping the onTakeFocus() call + * because some movement methods do something other than + * just setting the selection in theirs and we still + * need to go through that path. + */ + Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd); + } + + if (mSelectAllOnFocus) { + mTextView.selectAllText(); + } + + mTouchFocusSelected = true; + } + + mFrozenWithFocus = false; + mSelectionMoved = false; + + if (mError != null) { + showError(); + } + + makeBlink(); + } else { + if (mError != null) { + hideError(); + } + // Don't leave us in the middle of a batch edit. + mTextView.onEndBatchEdit(); + + if (mTextView instanceof ExtractEditText) { + // terminateTextSelectionMode removes selection, which we want to keep when + // ExtractEditText goes out of focus. + final int selStart = mTextView.getSelectionStart(); + final int selEnd = mTextView.getSelectionEnd(); + hideControllers(); + Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd); + } else { + hideControllers(); + downgradeEasyCorrectionSpans(); + } + + // No need to create the controller + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.resetTouchOffsets(); + } + } + } + + /** + * Downgrades to simple suggestions all the easy correction spans that are not a spell check + * span. + */ + private void downgradeEasyCorrectionSpans() { + CharSequence text = mTextView.getText(); + if (text instanceof Spannable) { + Spannable spannable = (Spannable) text; + SuggestionSpan[] suggestionSpans = spannable.getSpans(0, + spannable.length(), SuggestionSpan.class); + for (int i = 0; i < suggestionSpans.length; i++) { + int flags = suggestionSpans[i].getFlags(); + if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0 + && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) { + flags &= ~SuggestionSpan.FLAG_EASY_CORRECT; + suggestionSpans[i].setFlags(flags); + } + } + } + } + + void sendOnTextChanged(int start, int after) { + updateSpellCheckSpans(start, start + after, false); + + // Hide the controllers as soon as text is modified (typing, procedural...) + // We do not hide the span controllers, since they can be added when a new text is + // inserted into the text view (voice IME). + hideCursorControllers(); + } + + private int getLastTapPosition() { + // No need to create the controller at that point, no last tap position saved + if (mSelectionModifierCursorController != null) { + int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset(); + if (lastTapPosition >= 0) { + // Safety check, should not be possible. + if (lastTapPosition > mTextView.getText().length()) { + lastTapPosition = mTextView.getText().length(); + } + return lastTapPosition; + } + } + + return -1; + } + + void onWindowFocusChanged(boolean hasWindowFocus) { + if (hasWindowFocus) { + if (mBlink != null) { + mBlink.uncancel(); + makeBlink(); + } + } else { + if (mBlink != null) { + mBlink.cancel(); + } + if (mInputContentType != null) { + mInputContentType.enterDown = false; + } + // Order matters! Must be done before onParentLostFocus to rely on isShowingUp + hideControllers(); + if (mSuggestionsPopupWindow != null) { + mSuggestionsPopupWindow.onParentLostFocus(); + } + + // Don't leave us in the middle of a batch edit. + mTextView.onEndBatchEdit(); + } + } + + void onTouchEvent(MotionEvent event) { + if (hasSelectionController()) { + getSelectionController().onTouchEvent(event); + } + + if (mShowSuggestionRunnable != null) { + mTextView.removeCallbacks(mShowSuggestionRunnable); + mShowSuggestionRunnable = null; + } + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mLastDownPositionX = event.getX(); + mLastDownPositionY = event.getY(); + + // Reset this state; it will be re-set if super.onTouchEvent + // causes focus to move to the view. + mTouchFocusSelected = false; + mIgnoreActionUpEvent = false; + } + } + + public void beginBatchEdit() { + mInBatchEditControllers = true; + final InputMethodState ims = mInputMethodState; + if (ims != null) { + int nesting = ++ims.mBatchEditNesting; + if (nesting == 1) { + ims.mCursorChanged = false; + ims.mChangedDelta = 0; + if (ims.mContentChanged) { + // We already have a pending change from somewhere else, + // so turn this into a full update. + ims.mChangedStart = 0; + ims.mChangedEnd = mTextView.getText().length(); + } else { + ims.mChangedStart = EXTRACT_UNKNOWN; + ims.mChangedEnd = EXTRACT_UNKNOWN; + ims.mContentChanged = false; + } + mTextView.onBeginBatchEdit(); + } + } + } + + public void endBatchEdit() { + mInBatchEditControllers = false; + final InputMethodState ims = mInputMethodState; + if (ims != null) { + int nesting = --ims.mBatchEditNesting; + if (nesting == 0) { + finishBatchEdit(ims); + } + } + } + + void ensureEndedBatchEdit() { + final InputMethodState ims = mInputMethodState; + if (ims != null && ims.mBatchEditNesting != 0) { + ims.mBatchEditNesting = 0; + finishBatchEdit(ims); + } + } + + void finishBatchEdit(final InputMethodState ims) { + mTextView.onEndBatchEdit(); + + if (ims.mContentChanged || ims.mSelectionModeChanged) { + mTextView.updateAfterEdit(); + reportExtractedText(); + } else if (ims.mCursorChanged) { + // Cheezy way to get us to report the current cursor location. + mTextView.invalidateCursor(); + } + } + + static final int EXTRACT_NOTHING = -2; + static final int EXTRACT_UNKNOWN = -1; + + boolean extractText(ExtractedTextRequest request, ExtractedText outText) { + return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, + EXTRACT_UNKNOWN, outText); + } + + private boolean extractTextInternal(ExtractedTextRequest request, + int partialStartOffset, int partialEndOffset, int delta, + ExtractedText outText) { + final CharSequence content = mTextView.getText(); + if (content != null) { + if (partialStartOffset != EXTRACT_NOTHING) { + final int N = content.length(); + if (partialStartOffset < 0) { + outText.partialStartOffset = outText.partialEndOffset = -1; + partialStartOffset = 0; + partialEndOffset = N; + } else { + // Now use the delta to determine the actual amount of text + // we need. + partialEndOffset += delta; + // Adjust offsets to ensure we contain full spans. + if (content instanceof Spanned) { + Spanned spanned = (Spanned)content; + Object[] spans = spanned.getSpans(partialStartOffset, + partialEndOffset, ParcelableSpan.class); + int i = spans.length; + while (i > 0) { + i--; + int j = spanned.getSpanStart(spans[i]); + if (j < partialStartOffset) partialStartOffset = j; + j = spanned.getSpanEnd(spans[i]); + if (j > partialEndOffset) partialEndOffset = j; + } + } + outText.partialStartOffset = partialStartOffset; + outText.partialEndOffset = partialEndOffset - delta; + + if (partialStartOffset > N) { + partialStartOffset = N; + } else if (partialStartOffset < 0) { + partialStartOffset = 0; + } + if (partialEndOffset > N) { + partialEndOffset = N; + } else if (partialEndOffset < 0) { + partialEndOffset = 0; + } + } + if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { + outText.text = content.subSequence(partialStartOffset, + partialEndOffset); + } else { + outText.text = TextUtils.substring(content, partialStartOffset, + partialEndOffset); + } + } else { + outText.partialStartOffset = 0; + outText.partialEndOffset = 0; + outText.text = ""; + } + outText.flags = 0; + if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) { + outText.flags |= ExtractedText.FLAG_SELECTING; + } + if (mTextView.isSingleLine()) { + outText.flags |= ExtractedText.FLAG_SINGLE_LINE; + } + outText.startOffset = 0; + outText.selectionStart = mTextView.getSelectionStart(); + outText.selectionEnd = mTextView.getSelectionEnd(); + return true; + } + return false; + } + + boolean reportExtractedText() { + final Editor.InputMethodState ims = mInputMethodState; + if (ims != null) { + final boolean contentChanged = ims.mContentChanged; + if (contentChanged || ims.mSelectionModeChanged) { + ims.mContentChanged = false; + ims.mSelectionModeChanged = false; + final ExtractedTextRequest req = ims.mExtracting; + if (req != null) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG, + "Retrieving extracted start=" + ims.mChangedStart + + " end=" + ims.mChangedEnd + + " delta=" + ims.mChangedDelta); + if (ims.mChangedStart < 0 && !contentChanged) { + ims.mChangedStart = EXTRACT_NOTHING; + } + if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, + ims.mChangedDelta, ims.mTmpExtracted)) { + if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG, + "Reporting extracted start=" + + ims.mTmpExtracted.partialStartOffset + + " end=" + ims.mTmpExtracted.partialEndOffset + + ": " + ims.mTmpExtracted.text); + imm.updateExtractedText(mTextView, req.token, ims.mTmpExtracted); + ims.mChangedStart = EXTRACT_UNKNOWN; + ims.mChangedEnd = EXTRACT_UNKNOWN; + ims.mChangedDelta = 0; + ims.mContentChanged = false; + return true; + } + } + } + } + } + return false; + } + + void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint, + int cursorOffsetVertical) { + final int selectionStart = mTextView.getSelectionStart(); + final int selectionEnd = mTextView.getSelectionEnd(); + + final InputMethodState ims = mInputMethodState; + if (ims != null && ims.mBatchEditNesting == 0) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + if (imm.isActive(mTextView)) { + boolean reported = false; + if (ims.mContentChanged || ims.mSelectionModeChanged) { + // We are in extract mode and the content has changed + // in some way... just report complete new text to the + // input method. + reported = reportExtractedText(); + } + if (!reported && highlight != null) { + int candStart = -1; + int candEnd = -1; + if (mTextView.getText() instanceof Spannable) { + Spannable sp = (Spannable) mTextView.getText(); + candStart = EditableInputConnection.getComposingSpanStart(sp); + candEnd = EditableInputConnection.getComposingSpanEnd(sp); + } + imm.updateSelection(mTextView, + selectionStart, selectionEnd, candStart, candEnd); + } + } + + if (imm.isWatchingCursor(mTextView) && highlight != null) { + highlight.computeBounds(ims.mTmpRectF, true); + ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0; + + canvas.getMatrix().mapPoints(ims.mTmpOffset); + ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]); + + ims.mTmpRectF.offset(0, cursorOffsetVertical); + + ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5), + (int)(ims.mTmpRectF.top + 0.5), + (int)(ims.mTmpRectF.right + 0.5), + (int)(ims.mTmpRectF.bottom + 0.5)); + + imm.updateCursor(mTextView, + ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top, + ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom); + } + } + } + + if (mCorrectionHighlighter != null) { + mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); + } + + if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) { + drawCursor(canvas, cursorOffsetVertical); + // Rely on the drawable entirely, do not draw the cursor line. + // Has to be done after the IMM related code above which relies on the highlight. + highlight = null; + } + + if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) { + drawHardwareAccelerated(canvas, layout, highlight, highlightPaint, + cursorOffsetVertical); + } else { + layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical); + } + } + + private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, + Paint highlightPaint, int cursorOffsetVertical) { + final int width = mTextView.getWidth(); + final int height = mTextView.getHeight(); + + final long lineRange = layout.getLineRangeForDraw(canvas); + int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); + int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); + if (lastLine < 0) return; + + layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical, + firstLine, lastLine); + + if (layout instanceof DynamicLayout) { + if (mTextDisplayLists == null) { + mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)]; + } + + DynamicLayout dynamicLayout = (DynamicLayout) layout; + int[] blockEnds = dynamicLayout.getBlockEnds(); + int[] blockIndices = dynamicLayout.getBlockIndices(); + final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); + + final int mScrollX = mTextView.getScrollX(); + final int mScrollY = mTextView.getScrollY(); + canvas.translate(mScrollX, mScrollY); + int endOfPreviousBlock = -1; + int searchStartIndex = 0; + for (int i = 0; i < numberOfBlocks; i++) { + int blockEnd = blockEnds[i]; + int blockIndex = blockIndices[i]; + + final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX; + if (blockIsInvalid) { + blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks, + searchStartIndex); + // Dynamic layout internal block indices structure is updated from Editor + blockIndices[i] = blockIndex; + searchStartIndex = blockIndex + 1; + } + + DisplayList blockDisplayList = mTextDisplayLists[blockIndex]; + if (blockDisplayList == null) { + blockDisplayList = mTextDisplayLists[blockIndex] = + mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex); + } else { + if (blockIsInvalid) blockDisplayList.invalidate(); + } + + if (!blockDisplayList.isValid()) { + final HardwareCanvas hardwareCanvas = blockDisplayList.start(); + try { + hardwareCanvas.setViewport(width, height); + // The dirty rect should always be null for a display list + hardwareCanvas.onPreDraw(null); + hardwareCanvas.translate(-mScrollX, -mScrollY); + layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd); + hardwareCanvas.translate(mScrollX, mScrollY); + } finally { + hardwareCanvas.onPostDraw(); + blockDisplayList.end(); + if (View.USE_DISPLAY_LIST_PROPERTIES) { + blockDisplayList.setLeftTopRightBottom(0, 0, width, height); + } + } + } + + ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null, + DisplayList.FLAG_CLIP_CHILDREN); + endOfPreviousBlock = blockEnd; + } + canvas.translate(-mScrollX, -mScrollY); + } else { + // Boring layout is used for empty and hint text + layout.drawText(canvas, firstLine, lastLine); + } + } + + private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks, + int searchStartIndex) { + int length = mTextDisplayLists.length; + for (int i = searchStartIndex; i < length; i++) { + boolean blockIndexFound = false; + for (int j = 0; j < numberOfBlocks; j++) { + if (blockIndices[j] == i) { + blockIndexFound = true; + break; + } + } + if (blockIndexFound) continue; + return i; + } + + // No available index found, the pool has to grow + int newSize = ArrayUtils.idealIntArraySize(length + 1); + DisplayList[] displayLists = new DisplayList[newSize]; + System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length); + mTextDisplayLists = displayLists; + return length; + } + + private void drawCursor(Canvas canvas, int cursorOffsetVertical) { + final boolean translate = cursorOffsetVertical != 0; + if (translate) canvas.translate(0, cursorOffsetVertical); + for (int i = 0; i < mCursorCount; i++) { + mCursorDrawable[i].draw(canvas); + } + if (translate) canvas.translate(0, -cursorOffsetVertical); + } + + void invalidateTextDisplayList() { + if (mTextDisplayLists != null) { + for (int i = 0; i < mTextDisplayLists.length; i++) { + if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate(); + } + } + } + + void updateCursorsPositions() { + if (mTextView.mCursorDrawableRes == 0) { + mCursorCount = 0; + return; + } + + Layout layout = mTextView.getLayout(); + final int offset = mTextView.getSelectionStart(); + final int line = layout.getLineForOffset(offset); + final int top = layout.getLineTop(line); + final int bottom = layout.getLineTop(line + 1); + + mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1; + + int middle = bottom; + if (mCursorCount == 2) { + // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)} + middle = (top + bottom) >> 1; + } + + updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset)); + + if (mCursorCount == 2) { + updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset)); + } + } + + /** + * @return true if the selection mode was actually started. + */ + boolean startSelectionActionMode() { + if (mSelectionActionMode != null) { + // Selection action mode is already started + return false; + } + + if (!canSelectText() || !mTextView.requestFocus()) { + Log.w(TextView.LOG_TAG, + "TextView does not support text selection. Action mode cancelled."); + return false; + } + + if (!mTextView.hasSelection()) { + // There may already be a selection on device rotation + if (!selectCurrentWord()) { + // No word found under cursor or text selection not permitted. + return false; + } + } + + boolean willExtract = extractedTextModeWillBeStarted(); + + // Do not start the action mode when extracted text will show up full screen, which would + // immediately hide the newly created action bar and would be visually distracting. + if (!willExtract) { + ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); + mSelectionActionMode = mTextView.startActionMode(actionModeCallback); + } + + final boolean selectionStarted = mSelectionActionMode != null || willExtract; + if (selectionStarted && !mTextView.isTextSelectable()) { + // Show the IME to be able to replace text, except when selecting non editable text. + final InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.showSoftInput(mTextView, 0, null); + } + } + + return selectionStarted; + } + + private boolean extractedTextModeWillBeStarted() { + if (!(mTextView instanceof ExtractEditText)) { + final InputMethodManager imm = InputMethodManager.peekInstance(); + return imm != null && imm.isFullscreenMode(); + } + return false; + } + + /** + * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}. + */ + private boolean isCursorInsideSuggestionSpan() { + CharSequence text = mTextView.getText(); + if (!(text instanceof Spannable)) return false; + + SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans( + mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class); + return (suggestionSpans.length > 0); + } + + /** + * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with + * {@link SuggestionSpan#FLAG_EASY_CORRECT} set. + */ + private boolean isCursorInsideEasyCorrectionSpan() { + Spannable spannable = (Spannable) mTextView.getText(); + SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(), + mTextView.getSelectionEnd(), SuggestionSpan.class); + for (int i = 0; i < suggestionSpans.length; i++) { + if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) { + return true; + } + } + return false; + } + + void onTouchUpEvent(MotionEvent event) { + boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect(); + hideControllers(); + CharSequence text = mTextView.getText(); + if (!selectAllGotFocus && text.length() > 0) { + // Move cursor + final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); + Selection.setSelection((Spannable) text, offset); + if (mSpellChecker != null) { + // When the cursor moves, the word that was typed may need spell check + mSpellChecker.onSelectionChanged(); + } + if (!extractedTextModeWillBeStarted()) { + if (isCursorInsideEasyCorrectionSpan()) { + mShowSuggestionRunnable = new Runnable() { + public void run() { + showSuggestions(); + } + }; + // removeCallbacks is performed on every touch + mTextView.postDelayed(mShowSuggestionRunnable, + ViewConfiguration.getDoubleTapTimeout()); + } else if (hasInsertionController()) { + getInsertionController().show(); + } + } + } + } + + protected void stopSelectionActionMode() { + if (mSelectionActionMode != null) { + // This will hide the mSelectionModifierCursorController + mSelectionActionMode.finish(); + } + } + + /** + * @return True if this view supports insertion handles. + */ + boolean hasInsertionController() { + return mInsertionControllerEnabled; + } + + /** + * @return True if this view supports selection handles. + */ + boolean hasSelectionController() { + return mSelectionControllerEnabled; + } + + InsertionPointCursorController getInsertionController() { + if (!mInsertionControllerEnabled) { + return null; + } + + if (mInsertionPointCursorController == null) { + mInsertionPointCursorController = new InsertionPointCursorController(); + + final ViewTreeObserver observer = mTextView.getViewTreeObserver(); + observer.addOnTouchModeChangeListener(mInsertionPointCursorController); + } + + return mInsertionPointCursorController; + } + + SelectionModifierCursorController getSelectionController() { + if (!mSelectionControllerEnabled) { + return null; + } + + if (mSelectionModifierCursorController == null) { + mSelectionModifierCursorController = new SelectionModifierCursorController(); + + final ViewTreeObserver observer = mTextView.getViewTreeObserver(); + observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); + } + + return mSelectionModifierCursorController; + } + + private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { + if (mCursorDrawable[cursorIndex] == null) + mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable( + mTextView.mCursorDrawableRes); + + if (mTempRect == null) mTempRect = new Rect(); + mCursorDrawable[cursorIndex].getPadding(mTempRect); + final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth(); + horizontal = Math.max(0.5f, horizontal - 0.5f); + final int left = (int) (horizontal) - mTempRect.left; + mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width, + bottom + mTempRect.bottom); + } + + /** + * Called by the framework in response to a text auto-correction (such as fixing a typo using a + * a dictionnary) from the current input method, provided by it calling + * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default + * implementation flashes the background of the corrected word to provide feedback to the user. + * + * @param info The auto correct info about the text that was corrected. + */ + public void onCommitCorrection(CorrectionInfo info) { + if (mCorrectionHighlighter == null) { + mCorrectionHighlighter = new CorrectionHighlighter(); + } else { + mCorrectionHighlighter.invalidate(false); + } + + mCorrectionHighlighter.highlight(info); + } + + void showSuggestions() { + if (mSuggestionsPopupWindow == null) { + mSuggestionsPopupWindow = new SuggestionsPopupWindow(); + } + hideControllers(); + mSuggestionsPopupWindow.show(); + } + + boolean areSuggestionsShown() { + return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing(); + } + + void onScrollChanged() { + if (mPositionListener != null) { + mPositionListener.onScrollChanged(); + } + // Internal scroll affects the clip boundaries + invalidateTextDisplayList(); + } + + /** + * @return True when the TextView isFocused and has a valid zero-length selection (cursor). + */ + private boolean shouldBlink() { + if (!isCursorVisible() || !mTextView.isFocused()) return false; + + final int start = mTextView.getSelectionStart(); + if (start < 0) return false; + + final int end = mTextView.getSelectionEnd(); + if (end < 0) return false; + + return start == end; + } + + void makeBlink() { + if (shouldBlink()) { + mShowCursor = SystemClock.uptimeMillis(); + if (mBlink == null) mBlink = new Blink(); + mBlink.removeCallbacks(mBlink); + mBlink.postAtTime(mBlink, mShowCursor + BLINK); + } else { + if (mBlink != null) mBlink.removeCallbacks(mBlink); + } + } + + private class Blink extends Handler implements Runnable { + private boolean mCancelled; + + public void run() { + Log.d("GILLES", "blinking !!!"); + if (mCancelled) { + return; + } + + removeCallbacks(Blink.this); + + if (shouldBlink()) { + if (mTextView.getLayout() != null) { + mTextView.invalidateCursorPath(); + } + + postAtTime(this, SystemClock.uptimeMillis() + BLINK); + } + } + + void cancel() { + if (!mCancelled) { + removeCallbacks(Blink.this); + mCancelled = true; + } + } + + void uncancel() { + mCancelled = false; + } + } + + private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) { + TextView shadowView = (TextView) View.inflate(mTextView.getContext(), + com.android.internal.R.layout.text_drag_thumbnail, null); + + if (shadowView == null) { + throw new IllegalArgumentException("Unable to inflate text drag thumbnail"); + } + + if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) { + text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH); + } + shadowView.setText(text); + shadowView.setTextColor(mTextView.getTextColors()); + + shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge); + shadowView.setGravity(Gravity.CENTER); + + shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + shadowView.measure(size, size); + + shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight()); + shadowView.invalidate(); + return new DragShadowBuilder(shadowView); + } + + private static class DragLocalState { + public TextView sourceTextView; + public int start, end; + + public DragLocalState(TextView sourceTextView, int start, int end) { + this.sourceTextView = sourceTextView; + this.start = start; + this.end = end; + } + } + + void onDrop(DragEvent event) { + StringBuilder content = new StringBuilder(""); + ClipData clipData = event.getClipData(); + final int itemCount = clipData.getItemCount(); + for (int i=0; i < itemCount; i++) { + Item item = clipData.getItemAt(i); + content.append(item.coerceToText(mTextView.getContext())); + } + + final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); + + Object localState = event.getLocalState(); + DragLocalState dragLocalState = null; + if (localState instanceof DragLocalState) { + dragLocalState = (DragLocalState) localState; + } + boolean dragDropIntoItself = dragLocalState != null && + dragLocalState.sourceTextView == mTextView; + + if (dragDropIntoItself) { + if (offset >= dragLocalState.start && offset < dragLocalState.end) { + // A drop inside the original selection discards the drop. + return; + } + } + + final int originalLength = mTextView.getText().length(); + long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content); + int min = TextUtils.unpackRangeStartFromLong(minMax); + int max = TextUtils.unpackRangeEndFromLong(minMax); + + Selection.setSelection((Spannable) mTextView.getText(), max); + mTextView.replaceText_internal(min, max, content); + + if (dragDropIntoItself) { + int dragSourceStart = dragLocalState.start; + int dragSourceEnd = dragLocalState.end; + if (max <= dragSourceStart) { + // Inserting text before selection has shifted positions + final int shift = mTextView.getText().length() - originalLength; + dragSourceStart += shift; + dragSourceEnd += shift; + } + + // Delete original selection + mTextView.deleteText_internal(dragSourceStart, dragSourceEnd); + + // Make sure we do not leave two adjacent spaces. + CharSequence t = mTextView.getTransformedText(dragSourceStart - 1, dragSourceStart + 1); + if ( (dragSourceStart == 0 || Character.isSpaceChar(t.charAt(0))) && + (dragSourceStart == mTextView.getText().length() || + Character.isSpaceChar(t.charAt(1))) ) { + final int pos = dragSourceStart == mTextView.getText().length() ? + dragSourceStart - 1 : dragSourceStart; + mTextView.deleteText_internal(pos, pos + 1); + } + } + } + + /** + * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related + * pop-up should be displayed. + */ + class EasyEditSpanController implements TextWatcher { + + private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs + + private EasyEditPopupWindow mPopupWindow; + + private EasyEditSpan mEasyEditSpan; + + private Runnable mHidePopup; + + public void hide() { + if (mPopupWindow != null) { + mPopupWindow.hide(); + mTextView.removeCallbacks(mHidePopup); + } + removeSpans(mTextView.getText()); + mEasyEditSpan = null; + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Intentionally empty + } + + public void afterTextChanged(Editable s) { + // Intentionally empty + } + + /** + * Monitors the changes in the text. + * + * <p>{@link SpanWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used, + * as the notifications are not sent when a spannable (with spans) is inserted. + */ + public void onTextChanged(CharSequence buffer, int start, int before, int after) { + adjustSpans(buffer, start, after); + + if (mTextView.getWindowVisibility() != View.VISIBLE) { + // The window is not visible yet, ignore the text change. + return; + } + + if (mTextView.getLayout() == null) { + // The view has not been layout yet, ignore the text change + return; + } + + InputMethodManager imm = InputMethodManager.peekInstance(); + if (!(mTextView instanceof ExtractEditText) && imm != null && imm.isFullscreenMode()) { + // The input is in extract mode. We do not have to handle the easy edit in the + // original TextView, as the ExtractEditText will do + return; + } + + // Remove the current easy edit span, as the text changed, and remove the pop-up + // (if any) + if (mEasyEditSpan != null) { + if (buffer instanceof Spannable) { + ((Spannable) buffer).removeSpan(mEasyEditSpan); + } + mEasyEditSpan = null; + } + if (mPopupWindow != null && mPopupWindow.isShowing()) { + mPopupWindow.hide(); + } + + // Display the new easy edit span (if any). + if (buffer instanceof Spanned) { + mEasyEditSpan = getSpan((Spanned) buffer); + if (mEasyEditSpan != null) { + if (mPopupWindow == null) { + mPopupWindow = new EasyEditPopupWindow(); + mHidePopup = new Runnable() { + @Override + public void run() { + hide(); + } + }; + } + mPopupWindow.show(mEasyEditSpan); + mTextView.removeCallbacks(mHidePopup); + mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); + } + } + } + + /** + * Adjusts the spans by removing all of them except the last one. + */ + private void adjustSpans(CharSequence buffer, int start, int after) { + // This method enforces that only one easy edit span is attached to the text. + // A better way to enforce this would be to listen for onSpanAdded, but this method + // cannot be used in this scenario as no notification is triggered when a text with + // spans is inserted into a text. + if (buffer instanceof Spannable) { + Spannable spannable = (Spannable) buffer; + EasyEditSpan[] spans = spannable.getSpans(start, start + after, EasyEditSpan.class); + if (spans.length > 0) { + // Assuming there was only one EasyEditSpan before, we only need check to + // check for a duplicate if a new one is found in the modified interval + spans = spannable.getSpans(0, spannable.length(), EasyEditSpan.class); + for (int i = 1; i < spans.length; i++) { + spannable.removeSpan(spans[i]); + } + } + } + } + + /** + * Removes all the {@link EasyEditSpan} currently attached. + */ + private void removeSpans(CharSequence buffer) { + if (buffer instanceof Spannable) { + Spannable spannable = (Spannable) buffer; + EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(), + EasyEditSpan.class); + for (int i = 0; i < spans.length; i++) { + spannable.removeSpan(spans[i]); + } + } + } + + private EasyEditSpan getSpan(Spanned spanned) { + EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(), + EasyEditSpan.class); + if (easyEditSpans.length == 0) { + return null; + } else { + return easyEditSpans[0]; + } + } + } + + /** + * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled + * by {@link EasyEditSpanController}. + */ + private class EasyEditPopupWindow extends PinnedPopupWindow + implements OnClickListener { + private static final int POPUP_TEXT_LAYOUT = + com.android.internal.R.layout.text_edit_action_popup_text; + private TextView mDeleteTextView; + private EasyEditSpan mEasyEditSpan; + + @Override + protected void createPopupWindow() { + mPopupWindow = new PopupWindow(mTextView.getContext(), null, + com.android.internal.R.attr.textSelectHandleWindowStyle); + mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mPopupWindow.setClippingEnabled(true); + } + + @Override + protected void initContentView() { + LinearLayout linearLayout = new LinearLayout(mTextView.getContext()); + linearLayout.setOrientation(LinearLayout.HORIZONTAL); + mContentView = linearLayout; + mContentView.setBackgroundResource( + com.android.internal.R.drawable.text_edit_side_paste_window); + + LayoutInflater inflater = (LayoutInflater)mTextView.getContext(). + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + LayoutParams wrapContent = new LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); + mDeleteTextView.setLayoutParams(wrapContent); + mDeleteTextView.setText(com.android.internal.R.string.delete); + mDeleteTextView.setOnClickListener(this); + mContentView.addView(mDeleteTextView); + } + + public void show(EasyEditSpan easyEditSpan) { + mEasyEditSpan = easyEditSpan; + super.show(); + } + + @Override + public void onClick(View view) { + if (view == mDeleteTextView) { + Editable editable = (Editable) mTextView.getText(); + int start = editable.getSpanStart(mEasyEditSpan); + int end = editable.getSpanEnd(mEasyEditSpan); + if (start >= 0 && end >= 0) { + mTextView.deleteText_internal(start, end); + } + } + } + + @Override + protected int getTextOffset() { + // Place the pop-up at the end of the span + Editable editable = (Editable) mTextView.getText(); + return editable.getSpanEnd(mEasyEditSpan); + } + + @Override + protected int getVerticalLocalPosition(int line) { + return mTextView.getLayout().getLineBottom(line); + } + + @Override + protected int clipVertically(int positionY) { + // As we display the pop-up below the span, no vertical clipping is required. + return positionY; + } + } + + private class PositionListener implements ViewTreeObserver.OnPreDrawListener { + // 3 handles + // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) + private final int MAXIMUM_NUMBER_OF_LISTENERS = 6; + private TextViewPositionListener[] mPositionListeners = + new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; + private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; + private boolean mPositionHasChanged = true; + // Absolute position of the TextView with respect to its parent window + private int mPositionX, mPositionY; + private int mNumberOfListeners; + private boolean mScrollHasChanged; + final int[] mTempCoords = new int[2]; + + public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) { + if (mNumberOfListeners == 0) { + updatePosition(); + ViewTreeObserver vto = mTextView.getViewTreeObserver(); + vto.addOnPreDrawListener(this); + } + + int emptySlotIndex = -1; + for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { + TextViewPositionListener listener = mPositionListeners[i]; + if (listener == positionListener) { + return; + } else if (emptySlotIndex < 0 && listener == null) { + emptySlotIndex = i; + } + } + + mPositionListeners[emptySlotIndex] = positionListener; + mCanMove[emptySlotIndex] = canMove; + mNumberOfListeners++; + } + + public void removeSubscriber(TextViewPositionListener positionListener) { + for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { + if (mPositionListeners[i] == positionListener) { + mPositionListeners[i] = null; + mNumberOfListeners--; + break; + } + } + + if (mNumberOfListeners == 0) { + ViewTreeObserver vto = mTextView.getViewTreeObserver(); + vto.removeOnPreDrawListener(this); + } + } + + public int getPositionX() { + return mPositionX; + } + + public int getPositionY() { + return mPositionY; + } + + @Override + public boolean onPreDraw() { + updatePosition(); + + for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { + if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) { + TextViewPositionListener positionListener = mPositionListeners[i]; + if (positionListener != null) { + positionListener.updatePosition(mPositionX, mPositionY, + mPositionHasChanged, mScrollHasChanged); + } + } + } + + mScrollHasChanged = false; + return true; + } + + private void updatePosition() { + mTextView.getLocationInWindow(mTempCoords); + + mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY; + + mPositionX = mTempCoords[0]; + mPositionY = mTempCoords[1]; + } + + public void onScrollChanged() { + mScrollHasChanged = true; + } + } + + private abstract class PinnedPopupWindow implements TextViewPositionListener { + protected PopupWindow mPopupWindow; + protected ViewGroup mContentView; + int mPositionX, mPositionY; + + protected abstract void createPopupWindow(); + protected abstract void initContentView(); + protected abstract int getTextOffset(); + protected abstract int getVerticalLocalPosition(int line); + protected abstract int clipVertically(int positionY); + + public PinnedPopupWindow() { + createPopupWindow(); + + mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + + initContentView(); + + LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + mContentView.setLayoutParams(wrapContent); + + mPopupWindow.setContentView(mContentView); + } + + public void show() { + getPositionListener().addSubscriber(this, false /* offset is fixed */); + + computeLocalPosition(); + + final PositionListener positionListener = getPositionListener(); + updatePosition(positionListener.getPositionX(), positionListener.getPositionY()); + } + + protected void measureContent() { + final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); + mContentView.measure( + View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, + View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, + View.MeasureSpec.AT_MOST)); + } + + /* The popup window will be horizontally centered on the getTextOffset() and vertically + * positioned according to viewportToContentHorizontalOffset. + * + * This method assumes that mContentView has properly been measured from its content. */ + private void computeLocalPosition() { + measureContent(); + final int width = mContentView.getMeasuredWidth(); + final int offset = getTextOffset(); + mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f); + mPositionX += mTextView.viewportToContentHorizontalOffset(); + + final int line = mTextView.getLayout().getLineForOffset(offset); + mPositionY = getVerticalLocalPosition(line); + mPositionY += mTextView.viewportToContentVerticalOffset(); + } + + private void updatePosition(int parentPositionX, int parentPositionY) { + int positionX = parentPositionX + mPositionX; + int positionY = parentPositionY + mPositionY; + + positionY = clipVertically(positionY); + + // Horizontal clipping + final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); + final int width = mContentView.getMeasuredWidth(); + positionX = Math.min(displayMetrics.widthPixels - width, positionX); + positionX = Math.max(0, positionX); + + if (isShowing()) { + mPopupWindow.update(positionX, positionY, -1, -1); + } else { + mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, + positionX, positionY); + } + } + + public void hide() { + mPopupWindow.dismiss(); + getPositionListener().removeSubscriber(this); + } + + @Override + public void updatePosition(int parentPositionX, int parentPositionY, + boolean parentPositionChanged, boolean parentScrolled) { + // Either parentPositionChanged or parentScrolled is true, check if still visible + if (isShowing() && isOffsetVisible(getTextOffset())) { + if (parentScrolled) computeLocalPosition(); + updatePosition(parentPositionX, parentPositionY); + } else { + hide(); + } + } + + public boolean isShowing() { + return mPopupWindow.isShowing(); + } + } + + private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener { + private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; + private static final int ADD_TO_DICTIONARY = -1; + private static final int DELETE_TEXT = -2; + private SuggestionInfo[] mSuggestionInfos; + private int mNumberOfSuggestions; + private boolean mCursorWasVisibleBeforeSuggestions; + private boolean mIsShowingUp = false; + private SuggestionAdapter mSuggestionsAdapter; + private final Comparator<SuggestionSpan> mSuggestionSpanComparator; + private final HashMap<SuggestionSpan, Integer> mSpansLengths; + + private class CustomPopupWindow extends PopupWindow { + public CustomPopupWindow(Context context, int defStyle) { + super(context, null, defStyle); + } + + @Override + public void dismiss() { + super.dismiss(); + + getPositionListener().removeSubscriber(SuggestionsPopupWindow.this); + + // Safe cast since show() checks that mTextView.getText() is an Editable + ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan); + + mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions); + if (hasInsertionController()) { + getInsertionController().show(); + } + } + } + + public SuggestionsPopupWindow() { + mCursorWasVisibleBeforeSuggestions = mCursorVisible; + mSuggestionSpanComparator = new SuggestionSpanComparator(); + mSpansLengths = new HashMap<SuggestionSpan, Integer>(); + } + + @Override + protected void createPopupWindow() { + mPopupWindow = new CustomPopupWindow(mTextView.getContext(), + com.android.internal.R.attr.textSuggestionsWindowStyle); + mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mPopupWindow.setFocusable(true); + mPopupWindow.setClippingEnabled(false); + } + + @Override + protected void initContentView() { + ListView listView = new ListView(mTextView.getContext()); + mSuggestionsAdapter = new SuggestionAdapter(); + listView.setAdapter(mSuggestionsAdapter); + listView.setOnItemClickListener(this); + mContentView = listView; + + // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete + mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2]; + for (int i = 0; i < mSuggestionInfos.length; i++) { + mSuggestionInfos[i] = new SuggestionInfo(); + } + } + + public boolean isShowingUp() { + return mIsShowingUp; + } + + public void onParentLostFocus() { + mIsShowingUp = false; + } + + private class SuggestionInfo { + int suggestionStart, suggestionEnd; // range of actual suggestion within text + SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents + int suggestionIndex; // the index of this suggestion inside suggestionSpan + SpannableStringBuilder text = new SpannableStringBuilder(); + TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(), + android.R.style.TextAppearance_SuggestionHighlight); + } + + private class SuggestionAdapter extends BaseAdapter { + private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext(). + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + @Override + public int getCount() { + return mNumberOfSuggestions; + } + + @Override + public Object getItem(int position) { + return mSuggestionInfos[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView textView = (TextView) convertView; + + if (textView == null) { + textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout, + parent, false); + } + + final SuggestionInfo suggestionInfo = mSuggestionInfos[position]; + textView.setText(suggestionInfo.text); + + if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { + textView.setCompoundDrawablesWithIntrinsicBounds( + com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0); + } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) { + textView.setCompoundDrawablesWithIntrinsicBounds( + com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0); + } else { + textView.setCompoundDrawables(null, null, null, null); + } + + return textView; + } + } + + private class SuggestionSpanComparator implements Comparator<SuggestionSpan> { + public int compare(SuggestionSpan span1, SuggestionSpan span2) { + final int flag1 = span1.getFlags(); + final int flag2 = span2.getFlags(); + if (flag1 != flag2) { + // The order here should match what is used in updateDrawState + final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; + final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; + final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0; + final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0; + if (easy1 && !misspelled1) return -1; + if (easy2 && !misspelled2) return 1; + if (misspelled1) return -1; + if (misspelled2) return 1; + } + + return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue(); + } + } + + /** + * Returns the suggestion spans that cover the current cursor position. The suggestion + * spans are sorted according to the length of text that they are attached to. + */ + private SuggestionSpan[] getSuggestionSpans() { + int pos = mTextView.getSelectionStart(); + Spannable spannable = (Spannable) mTextView.getText(); + SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); + + mSpansLengths.clear(); + for (SuggestionSpan suggestionSpan : suggestionSpans) { + int start = spannable.getSpanStart(suggestionSpan); + int end = spannable.getSpanEnd(suggestionSpan); + mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start)); + } + + // The suggestions are sorted according to their types (easy correction first, then + // misspelled) and to the length of the text that they cover (shorter first). + Arrays.sort(suggestionSpans, mSuggestionSpanComparator); + return suggestionSpans; + } + + @Override + public void show() { + if (!(mTextView.getText() instanceof Editable)) return; + + if (updateSuggestions()) { + mCursorWasVisibleBeforeSuggestions = mCursorVisible; + mTextView.setCursorVisible(false); + mIsShowingUp = true; + super.show(); + } + } + + @Override + protected void measureContent() { + final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); + final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec( + displayMetrics.widthPixels, View.MeasureSpec.AT_MOST); + final int verticalMeasure = View.MeasureSpec.makeMeasureSpec( + displayMetrics.heightPixels, View.MeasureSpec.AT_MOST); + + int width = 0; + View view = null; + for (int i = 0; i < mNumberOfSuggestions; i++) { + view = mSuggestionsAdapter.getView(i, view, mContentView); + view.getLayoutParams().width = LayoutParams.WRAP_CONTENT; + view.measure(horizontalMeasure, verticalMeasure); + width = Math.max(width, view.getMeasuredWidth()); + } + + // Enforce the width based on actual text widths + mContentView.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + verticalMeasure); + + Drawable popupBackground = mPopupWindow.getBackground(); + if (popupBackground != null) { + if (mTempRect == null) mTempRect = new Rect(); + popupBackground.getPadding(mTempRect); + width += mTempRect.left + mTempRect.right; + } + mPopupWindow.setWidth(width); + } + + @Override + protected int getTextOffset() { + return mTextView.getSelectionStart(); + } + + @Override + protected int getVerticalLocalPosition(int line) { + return mTextView.getLayout().getLineBottom(line); + } + + @Override + protected int clipVertically(int positionY) { + final int height = mContentView.getMeasuredHeight(); + final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); + return Math.min(positionY, displayMetrics.heightPixels - height); + } + + @Override + public void hide() { + super.hide(); + } + + private boolean updateSuggestions() { + Spannable spannable = (Spannable) mTextView.getText(); + SuggestionSpan[] suggestionSpans = getSuggestionSpans(); + + final int nbSpans = suggestionSpans.length; + // Suggestions are shown after a delay: the underlying spans may have been removed + if (nbSpans == 0) return false; + + mNumberOfSuggestions = 0; + int spanUnionStart = mTextView.getText().length(); + int spanUnionEnd = 0; + + SuggestionSpan misspelledSpan = null; + int underlineColor = 0; + + for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { + SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; + final int spanStart = spannable.getSpanStart(suggestionSpan); + final int spanEnd = spannable.getSpanEnd(suggestionSpan); + spanUnionStart = Math.min(spanStart, spanUnionStart); + spanUnionEnd = Math.max(spanEnd, spanUnionEnd); + + if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) { + misspelledSpan = suggestionSpan; + } + + // The first span dictates the background color of the highlighted text + if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor(); + + String[] suggestions = suggestionSpan.getSuggestions(); + int nbSuggestions = suggestions.length; + for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { + String suggestion = suggestions[suggestionIndex]; + + boolean suggestionIsDuplicate = false; + for (int i = 0; i < mNumberOfSuggestions; i++) { + if (mSuggestionInfos[i].text.toString().equals(suggestion)) { + SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan; + final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan); + final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan); + if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) { + suggestionIsDuplicate = true; + break; + } + } + } + + if (!suggestionIsDuplicate) { + SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; + suggestionInfo.suggestionSpan = suggestionSpan; + suggestionInfo.suggestionIndex = suggestionIndex; + suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion); + + mNumberOfSuggestions++; + + if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) { + // Also end outer for loop + spanIndex = nbSpans; + break; + } + } + } + } + + for (int i = 0; i < mNumberOfSuggestions; i++) { + highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); + } + + // Add "Add to dictionary" item if there is a span with the misspelled flag + if (misspelledSpan != null) { + final int misspelledStart = spannable.getSpanStart(misspelledSpan); + final int misspelledEnd = spannable.getSpanEnd(misspelledSpan); + if (misspelledStart >= 0 && misspelledEnd > misspelledStart) { + SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; + suggestionInfo.suggestionSpan = misspelledSpan; + suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY; + suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView. + getContext().getString(com.android.internal.R.string.addToDictionary)); + suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + mNumberOfSuggestions++; + } + } + + // Delete item + SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; + suggestionInfo.suggestionSpan = null; + suggestionInfo.suggestionIndex = DELETE_TEXT; + suggestionInfo.text.replace(0, suggestionInfo.text.length(), + mTextView.getContext().getString(com.android.internal.R.string.deleteText)); + suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mNumberOfSuggestions++; + + if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); + if (underlineColor == 0) { + // Fallback on the default highlight color when the first span does not provide one + mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor); + } else { + final float BACKGROUND_TRANSPARENCY = 0.4f; + final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY); + mSuggestionRangeSpan.setBackgroundColor( + (underlineColor & 0x00FFFFFF) + (newAlpha << 24)); + } + spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + mSuggestionsAdapter.notifyDataSetChanged(); + return true; + } + + private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, + int unionEnd) { + final Spannable text = (Spannable) mTextView.getText(); + final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan); + final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan); + + // Adjust the start/end of the suggestion span + suggestionInfo.suggestionStart = spanStart - unionStart; + suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart + + suggestionInfo.text.length(); + + suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, + suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + // Add the text before and after the span. + final String textAsString = text.toString(); + suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart)); + suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd)); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Editable editable = (Editable) mTextView.getText(); + SuggestionInfo suggestionInfo = mSuggestionInfos[position]; + + if (suggestionInfo.suggestionIndex == DELETE_TEXT) { + final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan); + int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan); + if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { + // Do not leave two adjacent spaces after deletion, or one at beginning of text + if (spanUnionEnd < editable.length() && + Character.isSpaceChar(editable.charAt(spanUnionEnd)) && + (spanUnionStart == 0 || + Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) { + spanUnionEnd = spanUnionEnd + 1; + } + mTextView.deleteText_internal(spanUnionStart, spanUnionEnd); + } + hide(); + return; + } + + final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan); + final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan); + if (spanStart < 0 || spanEnd <= spanStart) { + // Span has been removed + hide(); + return; + } + + final String originalText = editable.toString().substring(spanStart, spanEnd); + + if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { + Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); + intent.putExtra("word", originalText); + intent.putExtra("locale", mTextView.getTextServicesLocale().toString()); + intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + mTextView.getContext().startActivity(intent); + // There is no way to know if the word was indeed added. Re-check. + // TODO The ExtractEditText should remove the span in the original text instead + editable.removeSpan(suggestionInfo.suggestionSpan); + updateSpellCheckSpans(spanStart, spanEnd, false); + } else { + // SuggestionSpans are removed by replace: save them before + SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, + SuggestionSpan.class); + final int length = suggestionSpans.length; + int[] suggestionSpansStarts = new int[length]; + int[] suggestionSpansEnds = new int[length]; + int[] suggestionSpansFlags = new int[length]; + for (int i = 0; i < length; i++) { + final SuggestionSpan suggestionSpan = suggestionSpans[i]; + suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); + suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); + suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); + + // Remove potential misspelled flags + int suggestionSpanFlags = suggestionSpan.getFlags(); + if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) { + suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; + suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; + suggestionSpan.setFlags(suggestionSpanFlags); + } + } + + final int suggestionStart = suggestionInfo.suggestionStart; + final int suggestionEnd = suggestionInfo.suggestionEnd; + final String suggestion = suggestionInfo.text.subSequence( + suggestionStart, suggestionEnd).toString(); + mTextView.replaceText_internal(spanStart, spanEnd, suggestion); + + // Notify source IME of the suggestion pick. Do this before swaping texts. + if (!TextUtils.isEmpty( + suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText, + suggestionInfo.suggestionIndex); + } + } + + // Swap text content between actual text and Suggestion span + String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); + suggestions[suggestionInfo.suggestionIndex] = originalText; + + // Restore previous SuggestionSpans + final int lengthDifference = suggestion.length() - (spanEnd - spanStart); + for (int i = 0; i < length; i++) { + // Only spans that include the modified region make sense after replacement + // Spans partially included in the replaced region are removed, there is no + // way to assign them a valid range after replacement + if (suggestionSpansStarts[i] <= spanStart && + suggestionSpansEnds[i] >= spanEnd) { + mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], + suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]); + } + } + + // Move cursor at the end of the replaced word + final int newCursorPosition = spanEnd + lengthDifference; + mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition); + } + + hide(); + } + } + + /** + * An ActionMode Callback class that is used to provide actions while in text selection mode. + * + * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending + * on which of these this TextView supports. + */ + private class SelectionActionModeCallback implements ActionMode.Callback { + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes( + com.android.internal.R.styleable.SelectionModeDrawables); + + boolean allowText = mTextView.getContext().getResources().getBoolean( + com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon); + + mode.setTitle(mTextView.getContext().getString( + com.android.internal.R.string.textSelectionCABTitle)); + mode.setSubtitle(null); + mode.setTitleOptionalHint(true); + + int selectAllIconId = 0; // No icon by default + if (!allowText) { + // Provide an icon, text will not be displayed on smaller screens. + selectAllIconId = styledAttributes.getResourceId( + R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0); + } + + menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). + setIcon(selectAllIconId). + setAlphabeticShortcut('a'). + setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + + if (mTextView.canCut()) { + menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut). + setIcon(styledAttributes.getResourceId( + R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)). + setAlphabeticShortcut('x'). + setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + + if (mTextView.canCopy()) { + menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy). + setIcon(styledAttributes.getResourceId( + R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)). + setAlphabeticShortcut('c'). + setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + + if (mTextView.canPaste()) { + menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste). + setIcon(styledAttributes.getResourceId( + R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)). + setAlphabeticShortcut('v'). + setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + + styledAttributes.recycle(); + + if (mCustomSelectionActionModeCallback != null) { + if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) { + // The custom mode can choose to cancel the action mode + return false; + } + } + + if (menu.hasVisibleItems() || mode.getCustomView() != null) { + getSelectionController().show(); + return true; + } else { + return false; + } + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + if (mCustomSelectionActionModeCallback != null) { + return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu); + } + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (mCustomSelectionActionModeCallback != null && + mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) { + return true; + } + return mTextView.onTextContextMenuItem(item.getItemId()); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + if (mCustomSelectionActionModeCallback != null) { + mCustomSelectionActionModeCallback.onDestroyActionMode(mode); + } + Selection.setSelection((Spannable) mTextView.getText(), mTextView.getSelectionEnd()); + + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.hide(); + } + + mSelectionActionMode = null; + } + } + + private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener { + private static final int POPUP_TEXT_LAYOUT = + com.android.internal.R.layout.text_edit_action_popup_text; + private TextView mPasteTextView; + private TextView mReplaceTextView; + + @Override + protected void createPopupWindow() { + mPopupWindow = new PopupWindow(mTextView.getContext(), null, + com.android.internal.R.attr.textSelectHandleWindowStyle); + mPopupWindow.setClippingEnabled(true); + } + + @Override + protected void initContentView() { + LinearLayout linearLayout = new LinearLayout(mTextView.getContext()); + linearLayout.setOrientation(LinearLayout.HORIZONTAL); + mContentView = linearLayout; + mContentView.setBackgroundResource( + com.android.internal.R.drawable.text_edit_paste_window); + + LayoutInflater inflater = (LayoutInflater) mTextView.getContext(). + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + LayoutParams wrapContent = new LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); + mPasteTextView.setLayoutParams(wrapContent); + mContentView.addView(mPasteTextView); + mPasteTextView.setText(com.android.internal.R.string.paste); + mPasteTextView.setOnClickListener(this); + + mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); + mReplaceTextView.setLayoutParams(wrapContent); + mContentView.addView(mReplaceTextView); + mReplaceTextView.setText(com.android.internal.R.string.replace); + mReplaceTextView.setOnClickListener(this); + } + + @Override + public void show() { + boolean canPaste = mTextView.canPaste(); + boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan(); + mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE); + mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE); + + if (!canPaste && !canSuggest) return; + + super.show(); + } + + @Override + public void onClick(View view) { + if (view == mPasteTextView && mTextView.canPaste()) { + mTextView.onTextContextMenuItem(TextView.ID_PASTE); + hide(); + } else if (view == mReplaceTextView) { + int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; + stopSelectionActionMode(); + Selection.setSelection((Spannable) mTextView.getText(), middle); + showSuggestions(); + } + } + + @Override + protected int getTextOffset() { + return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; + } + + @Override + protected int getVerticalLocalPosition(int line) { + return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight(); + } + + @Override + protected int clipVertically(int positionY) { + if (positionY < 0) { + final int offset = getTextOffset(); + final Layout layout = mTextView.getLayout(); + final int line = layout.getLineForOffset(offset); + positionY += layout.getLineBottom(line) - layout.getLineTop(line); + positionY += mContentView.getMeasuredHeight(); + + // Assumes insertion and selection handles share the same height + final Drawable handle = mTextView.getResources().getDrawable( + mTextView.mTextSelectHandleRes); + positionY += handle.getIntrinsicHeight(); + } + + return positionY; + } + } + + private abstract class HandleView extends View implements TextViewPositionListener { + protected Drawable mDrawable; + protected Drawable mDrawableLtr; + protected Drawable mDrawableRtl; + private final PopupWindow mContainer; + // Position with respect to the parent TextView + private int mPositionX, mPositionY; + private boolean mIsDragging; + // Offset from touch position to mPosition + private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; + protected int mHotspotX; + // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up + private float mTouchOffsetY; + // Where the touch position should be on the handle to ensure a maximum cursor visibility + private float mIdealVerticalOffset; + // Parent's (TextView) previous position in window + private int mLastParentX, mLastParentY; + // Transient action popup window for Paste and Replace actions + protected ActionPopupWindow mActionPopupWindow; + // Previous text character offset + private int mPreviousOffset = -1; + // Previous text character offset + private boolean mPositionHasChanged = true; + // Used to delay the appearance of the action popup window + private Runnable mActionPopupShower; + + public HandleView(Drawable drawableLtr, Drawable drawableRtl) { + super(mTextView.getContext()); + mContainer = new PopupWindow(mTextView.getContext(), null, + com.android.internal.R.attr.textSelectHandleWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + mContainer.setContentView(this); + + mDrawableLtr = drawableLtr; + mDrawableRtl = drawableRtl; + + updateDrawable(); + + final int handleHeight = mDrawable.getIntrinsicHeight(); + mTouchOffsetY = -0.3f * handleHeight; + mIdealVerticalOffset = 0.7f * handleHeight; + } + + protected void updateDrawable() { + final int offset = getCurrentCursorOffset(); + final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset); + mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr; + mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset); + } + + protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun); + + // Touch-up filter: number of previous positions remembered + private static final int HISTORY_SIZE = 5; + private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150; + private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350; + private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE]; + private final int[] mPreviousOffsets = new int[HISTORY_SIZE]; + private int mPreviousOffsetIndex = 0; + private int mNumberPreviousOffsets = 0; + + private void startTouchUpFilter(int offset) { + mNumberPreviousOffsets = 0; + addPositionToTouchUpFilter(offset); + } + + private void addPositionToTouchUpFilter(int offset) { + mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE; + mPreviousOffsets[mPreviousOffsetIndex] = offset; + mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis(); + mNumberPreviousOffsets++; + } + + private void filterOnTouchUp() { + final long now = SystemClock.uptimeMillis(); + int i = 0; + int index = mPreviousOffsetIndex; + final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE); + while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) { + i++; + index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE; + } + + if (i > 0 && i < iMax && + (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { + positionAtCursorOffset(mPreviousOffsets[index], false); + } + } + + public boolean offsetHasBeenChanged() { + return mNumberPreviousOffsets > 1; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); + } + + public void show() { + if (isShowing()) return; + + getPositionListener().addSubscriber(this, true /* local position may change */); + + // Make sure the offset is always considered new, even when focusing at same position + mPreviousOffset = -1; + positionAtCursorOffset(getCurrentCursorOffset(), false); + + hideActionPopupWindow(); + } + + protected void dismiss() { + mIsDragging = false; + mContainer.dismiss(); + onDetached(); + } + + public void hide() { + dismiss(); + + getPositionListener().removeSubscriber(this); + } + + void showActionPopupWindow(int delay) { + if (mActionPopupWindow == null) { + mActionPopupWindow = new ActionPopupWindow(); + } + if (mActionPopupShower == null) { + mActionPopupShower = new Runnable() { + public void run() { + mActionPopupWindow.show(); + } + }; + } else { + mTextView.removeCallbacks(mActionPopupShower); + } + mTextView.postDelayed(mActionPopupShower, delay); + } + + protected void hideActionPopupWindow() { + if (mActionPopupShower != null) { + mTextView.removeCallbacks(mActionPopupShower); + } + if (mActionPopupWindow != null) { + mActionPopupWindow.hide(); + } + } + + public boolean isShowing() { + return mContainer.isShowing(); + } + + private boolean isVisible() { + // Always show a dragging handle. + if (mIsDragging) { + return true; + } + + if (mTextView.isInBatchEditMode()) { + return false; + } + + return isPositionVisible(mPositionX + mHotspotX, mPositionY); + } + + public abstract int getCurrentCursorOffset(); + + protected abstract void updateSelection(int offset); + + public abstract void updatePosition(float x, float y); + + protected void positionAtCursorOffset(int offset, boolean parentScrolled) { + // A HandleView relies on the layout, which may be nulled by external methods + Layout layout = mTextView.getLayout(); + if (layout == null) { + // Will update controllers' state, hiding them and stopping selection mode if needed + prepareCursorControllers(); + return; + } + + boolean offsetChanged = offset != mPreviousOffset; + if (offsetChanged || parentScrolled) { + if (offsetChanged) { + updateSelection(offset); + addPositionToTouchUpFilter(offset); + } + final int line = layout.getLineForOffset(offset); + + mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX); + mPositionY = layout.getLineBottom(line); + + // Take TextView's padding and scroll into account. + mPositionX += mTextView.viewportToContentHorizontalOffset(); + mPositionY += mTextView.viewportToContentVerticalOffset(); + + mPreviousOffset = offset; + mPositionHasChanged = true; + } + } + + public void updatePosition(int parentPositionX, int parentPositionY, + boolean parentPositionChanged, boolean parentScrolled) { + positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled); + if (parentPositionChanged || mPositionHasChanged) { + if (mIsDragging) { + // Update touchToWindow offset in case of parent scrolling while dragging + if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) { + mTouchToWindowOffsetX += parentPositionX - mLastParentX; + mTouchToWindowOffsetY += parentPositionY - mLastParentY; + mLastParentX = parentPositionX; + mLastParentY = parentPositionY; + } + + onHandleMoved(); + } + + if (isVisible()) { + final int positionX = parentPositionX + mPositionX; + final int positionY = parentPositionY + mPositionY; + if (isShowing()) { + mContainer.update(positionX, positionY, -1, -1); + } else { + mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, + positionX, positionY); + } + } else { + if (isShowing()) { + dismiss(); + } + } + + mPositionHasChanged = false; + } + } + + @Override + protected void onDraw(Canvas c) { + mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop); + mDrawable.draw(c); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + startTouchUpFilter(getCurrentCursorOffset()); + mTouchToWindowOffsetX = ev.getRawX() - mPositionX; + mTouchToWindowOffsetY = ev.getRawY() - mPositionY; + + final PositionListener positionListener = getPositionListener(); + mLastParentX = positionListener.getPositionX(); + mLastParentY = positionListener.getPositionY(); + mIsDragging = true; + break; + } + + case MotionEvent.ACTION_MOVE: { + final float rawX = ev.getRawX(); + final float rawY = ev.getRawY(); + + // Vertical hysteresis: vertical down movement tends to snap to ideal offset + final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY; + final float currentVerticalOffset = rawY - mPositionY - mLastParentY; + float newVerticalOffset; + if (previousVerticalOffset < mIdealVerticalOffset) { + newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset); + newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset); + } else { + newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset); + newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset); + } + mTouchToWindowOffsetY = newVerticalOffset + mLastParentY; + + final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; + final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY; + + updatePosition(newPosX, newPosY); + break; + } + + case MotionEvent.ACTION_UP: + filterOnTouchUp(); + mIsDragging = false; + break; + + case MotionEvent.ACTION_CANCEL: + mIsDragging = false; + break; + } + return true; + } + + public boolean isDragging() { + return mIsDragging; + } + + void onHandleMoved() { + hideActionPopupWindow(); + } + + public void onDetached() { + hideActionPopupWindow(); + } + } + + private class InsertionHandleView extends HandleView { + private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; + private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds + + // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow + private float mDownPositionX, mDownPositionY; + private Runnable mHider; + + public InsertionHandleView(Drawable drawable) { + super(drawable, drawable); + } + + @Override + public void show() { + super.show(); + + final long durationSinceCutOrCopy = + SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME; + if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { + showActionPopupWindow(0); + } + + hideAfterDelay(); + } + + public void showWithActionPopup() { + show(); + showActionPopupWindow(0); + } + + private void hideAfterDelay() { + if (mHider == null) { + mHider = new Runnable() { + public void run() { + hide(); + } + }; + } else { + removeHiderCallback(); + } + mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT); + } + + private void removeHiderCallback() { + if (mHider != null) { + mTextView.removeCallbacks(mHider); + } + } + + @Override + protected int getHotspotX(Drawable drawable, boolean isRtlRun) { + return drawable.getIntrinsicWidth() / 2; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final boolean result = super.onTouchEvent(ev); + + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mDownPositionX = ev.getRawX(); + mDownPositionY = ev.getRawY(); + break; + + case MotionEvent.ACTION_UP: + if (!offsetHasBeenChanged()) { + final float deltaX = mDownPositionX - ev.getRawX(); + final float deltaY = mDownPositionY - ev.getRawY(); + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + mTextView.getContext()); + final int touchSlop = viewConfiguration.getScaledTouchSlop(); + + if (distanceSquared < touchSlop * touchSlop) { + if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) { + // Tapping on the handle dismisses the displayed action popup + mActionPopupWindow.hide(); + } else { + showWithActionPopup(); + } + } + } + hideAfterDelay(); + break; + + case MotionEvent.ACTION_CANCEL: + hideAfterDelay(); + break; + + default: + break; + } + + return result; + } + + @Override + public int getCurrentCursorOffset() { + return mTextView.getSelectionStart(); + } + + @Override + public void updateSelection(int offset) { + Selection.setSelection((Spannable) mTextView.getText(), offset); + } + + @Override + public void updatePosition(float x, float y) { + positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false); + } + + @Override + void onHandleMoved() { + super.onHandleMoved(); + removeHiderCallback(); + } + + @Override + public void onDetached() { + super.onDetached(); + removeHiderCallback(); + } + } + + private class SelectionStartHandleView extends HandleView { + + public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) { + super(drawableLtr, drawableRtl); + } + + @Override + protected int getHotspotX(Drawable drawable, boolean isRtlRun) { + if (isRtlRun) { + return drawable.getIntrinsicWidth() / 4; + } else { + return (drawable.getIntrinsicWidth() * 3) / 4; + } + } + + @Override + public int getCurrentCursorOffset() { + return mTextView.getSelectionStart(); + } + + @Override + public void updateSelection(int offset) { + Selection.setSelection((Spannable) mTextView.getText(), offset, + mTextView.getSelectionEnd()); + updateDrawable(); + } + + @Override + public void updatePosition(float x, float y) { + int offset = mTextView.getOffsetForPosition(x, y); + + // Handles can not cross and selection is at least one character + final int selectionEnd = mTextView.getSelectionEnd(); + if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1); + + positionAtCursorOffset(offset, false); + } + + public ActionPopupWindow getActionPopupWindow() { + return mActionPopupWindow; + } + } + + private class SelectionEndHandleView extends HandleView { + + public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) { + super(drawableLtr, drawableRtl); + } + + @Override + protected int getHotspotX(Drawable drawable, boolean isRtlRun) { + if (isRtlRun) { + return (drawable.getIntrinsicWidth() * 3) / 4; + } else { + return drawable.getIntrinsicWidth() / 4; + } + } + + @Override + public int getCurrentCursorOffset() { + return mTextView.getSelectionEnd(); + } + + @Override + public void updateSelection(int offset) { + Selection.setSelection((Spannable) mTextView.getText(), + mTextView.getSelectionStart(), offset); + updateDrawable(); + } + + @Override + public void updatePosition(float x, float y) { + int offset = mTextView.getOffsetForPosition(x, y); + + // Handles can not cross and selection is at least one character + final int selectionStart = mTextView.getSelectionStart(); + if (offset <= selectionStart) { + offset = Math.min(selectionStart + 1, mTextView.getText().length()); + } + + positionAtCursorOffset(offset, false); + } + + public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) { + mActionPopupWindow = actionPopupWindow; + } + } + + /** + * A CursorController instance can be used to control a cursor in the text. + */ + private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { + /** + * Makes the cursor controller visible on screen. + * See also {@link #hide()}. + */ + public void show(); + + /** + * Hide the cursor controller from screen. + * See also {@link #show()}. + */ + public void hide(); + + /** + * Called when the view is detached from window. Perform house keeping task, such as + * stopping Runnable thread that would otherwise keep a reference on the context, thus + * preventing the activity from being recycled. + */ + public void onDetached(); + } + + private class InsertionPointCursorController implements CursorController { + private InsertionHandleView mHandle; + + public void show() { + getHandle().show(); + } + + public void showWithActionPopup() { + getHandle().showWithActionPopup(); + } + + public void hide() { + if (mHandle != null) { + mHandle.hide(); + } + } + + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode) { + hide(); + } + } + + private InsertionHandleView getHandle() { + if (mSelectHandleCenter == null) { + mSelectHandleCenter = mTextView.getResources().getDrawable( + mTextView.mTextSelectHandleRes); + } + if (mHandle == null) { + mHandle = new InsertionHandleView(mSelectHandleCenter); + } + return mHandle; + } + + @Override + public void onDetached() { + final ViewTreeObserver observer = mTextView.getViewTreeObserver(); + observer.removeOnTouchModeChangeListener(this); + + if (mHandle != null) mHandle.onDetached(); + } + } + + class SelectionModifierCursorController implements CursorController { + private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds + // The cursor controller handles, lazily created when shown. + private SelectionStartHandleView mStartHandle; + private SelectionEndHandleView mEndHandle; + // The offsets of that last touch down event. Remembered to start selection there. + private int mMinTouchOffset, mMaxTouchOffset; + + // Double tap detection + private long mPreviousTapUpTime = 0; + private float mDownPositionX, mDownPositionY; + private boolean mGestureStayedInTapRegion; + + SelectionModifierCursorController() { + resetTouchOffsets(); + } + + public void show() { + if (mTextView.isInBatchEditMode()) { + return; + } + initDrawables(); + initHandles(); + hideInsertionPointCursorController(); + } + + private void initDrawables() { + if (mSelectHandleLeft == null) { + mSelectHandleLeft = mTextView.getContext().getResources().getDrawable( + mTextView.mTextSelectHandleLeftRes); + } + if (mSelectHandleRight == null) { + mSelectHandleRight = mTextView.getContext().getResources().getDrawable( + mTextView.mTextSelectHandleRightRes); + } + } + + private void initHandles() { + // Lazy object creation has to be done before updatePosition() is called. + if (mStartHandle == null) { + mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight); + } + if (mEndHandle == null) { + mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft); + } + + mStartHandle.show(); + mEndHandle.show(); + + // Make sure both left and right handles share the same ActionPopupWindow (so that + // moving any of the handles hides the action popup). + mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION); + mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow()); + + hideInsertionPointCursorController(); + } + + public void hide() { + if (mStartHandle != null) mStartHandle.hide(); + if (mEndHandle != null) mEndHandle.hide(); + } + + public void onTouchEvent(MotionEvent event) { + // This is done even when the View does not have focus, so that long presses can start + // selection and tap can move cursor from this tap position. + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + final float x = event.getX(); + final float y = event.getY(); + + // Remember finger down position, to be able to start selection from there + mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y); + + // Double tap detection + if (mGestureStayedInTapRegion) { + long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; + if (duration <= ViewConfiguration.getDoubleTapTimeout()) { + final float deltaX = x - mDownPositionX; + final float deltaY = y - mDownPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + ViewConfiguration viewConfiguration = ViewConfiguration.get( + mTextView.getContext()); + int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); + boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop; + + if (stayedInArea && isPositionOnText(x, y)) { + startSelectionActionMode(); + mDiscardNextActionUp = true; + } + } + } + + mDownPositionX = x; + mDownPositionY = y; + mGestureStayedInTapRegion = true; + break; + + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_POINTER_UP: + // Handle multi-point gestures. Keep min and max offset positions. + // Only activated for devices that correctly handle multi-touch. + if (mTextView.getContext().getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { + updateMinAndMaxOffsets(event); + } + break; + + case MotionEvent.ACTION_MOVE: + if (mGestureStayedInTapRegion) { + final float deltaX = event.getX() - mDownPositionX; + final float deltaY = event.getY() - mDownPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + mTextView.getContext()); + int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); + + if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { + mGestureStayedInTapRegion = false; + } + } + break; + + case MotionEvent.ACTION_UP: + mPreviousTapUpTime = SystemClock.uptimeMillis(); + break; + } + } + + /** + * @param event + */ + private void updateMinAndMaxOffsets(MotionEvent event) { + int pointerCount = event.getPointerCount(); + for (int index = 0; index < pointerCount; index++) { + int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index)); + if (offset < mMinTouchOffset) mMinTouchOffset = offset; + if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; + } + } + + public int getMinTouchOffset() { + return mMinTouchOffset; + } + + public int getMaxTouchOffset() { + return mMaxTouchOffset; + } + + public void resetTouchOffsets() { + mMinTouchOffset = mMaxTouchOffset = -1; + } + + /** + * @return true iff this controller is currently used to move the selection start. + */ + public boolean isSelectionStartDragged() { + return mStartHandle != null && mStartHandle.isDragging(); + } + + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode) { + hide(); + } + } + + @Override + public void onDetached() { + final ViewTreeObserver observer = mTextView.getViewTreeObserver(); + observer.removeOnTouchModeChangeListener(this); + + if (mStartHandle != null) mStartHandle.onDetached(); + if (mEndHandle != null) mEndHandle.onDetached(); + } + } + + private class CorrectionHighlighter { + private final Path mPath = new Path(); + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int mStart, mEnd; + private long mFadingStartTime; + private RectF mTempRectF; + private final static int FADE_OUT_DURATION = 400; + + public CorrectionHighlighter() { + mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo(). + applicationScale); + mPaint.setStyle(Paint.Style.FILL); + } + + public void highlight(CorrectionInfo info) { + mStart = info.getOffset(); + mEnd = mStart + info.getNewText().length(); + mFadingStartTime = SystemClock.uptimeMillis(); + + if (mStart < 0 || mEnd < 0) { + stopAnimation(); + } + } + + public void draw(Canvas canvas, int cursorOffsetVertical) { + if (updatePath() && updatePaint()) { + if (cursorOffsetVertical != 0) { + canvas.translate(0, cursorOffsetVertical); + } + + canvas.drawPath(mPath, mPaint); + + if (cursorOffsetVertical != 0) { + canvas.translate(0, -cursorOffsetVertical); + } + invalidate(true); // TODO invalidate cursor region only + } else { + stopAnimation(); + invalidate(false); // TODO invalidate cursor region only + } + } + + private boolean updatePaint() { + final long duration = SystemClock.uptimeMillis() - mFadingStartTime; + if (duration > FADE_OUT_DURATION) return false; + + final float coef = 1.0f - (float) duration / FADE_OUT_DURATION; + final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor); + final int color = (mTextView.mHighlightColor & 0x00FFFFFF) + + ((int) (highlightColorAlpha * coef) << 24); + mPaint.setColor(color); + return true; + } + + private boolean updatePath() { + final Layout layout = mTextView.getLayout(); + if (layout == null) return false; + + // Update in case text is edited while the animation is run + final int length = mTextView.getText().length(); + int start = Math.min(length, mStart); + int end = Math.min(length, mEnd); + + mPath.reset(); + layout.getSelectionPath(start, end, mPath); + return true; + } + + private void invalidate(boolean delayed) { + if (mTextView.getLayout() == null) return; + + if (mTempRectF == null) mTempRectF = new RectF(); + mPath.computeBounds(mTempRectF, false); + + int left = mTextView.getCompoundPaddingLeft(); + int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true); + + if (delayed) { + mTextView.postInvalidateOnAnimation( + left + (int) mTempRectF.left, top + (int) mTempRectF.top, + left + (int) mTempRectF.right, top + (int) mTempRectF.bottom); + } else { + mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top, + (int) mTempRectF.right, (int) mTempRectF.bottom); + } + } + + private void stopAnimation() { + Editor.this.mCorrectionHighlighter = null; + } + } + + private static class ErrorPopup extends PopupWindow { + private boolean mAbove = false; + private final TextView mView; + private int mPopupInlineErrorBackgroundId = 0; + private int mPopupInlineErrorAboveBackgroundId = 0; + + ErrorPopup(TextView v, int width, int height) { + super(v, width, height); + mView = v; + // Make sure the TextView has a background set as it will be used the first time it is + // shown and positionned. Initialized with below background, which should have + // dimensions identical to the above version for this to work (and is more likely). + mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, + com.android.internal.R.styleable.Theme_errorMessageBackground); + mView.setBackgroundResource(mPopupInlineErrorBackgroundId); + } + + void fixDirection(boolean above) { + mAbove = above; + + if (above) { + mPopupInlineErrorAboveBackgroundId = + getResourceId(mPopupInlineErrorAboveBackgroundId, + com.android.internal.R.styleable.Theme_errorMessageAboveBackground); + } else { + mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, + com.android.internal.R.styleable.Theme_errorMessageBackground); + } + + mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId : + mPopupInlineErrorBackgroundId); + } + + private int getResourceId(int currentId, int index) { + if (currentId == 0) { + TypedArray styledAttributes = mView.getContext().obtainStyledAttributes( + R.styleable.Theme); + currentId = styledAttributes.getResourceId(index, 0); + styledAttributes.recycle(); + } + return currentId; + } + + @Override + public void update(int x, int y, int w, int h, boolean force) { + super.update(x, y, w, h, force); + + boolean above = isAboveAnchor(); + if (above != mAbove) { + fixDirection(above); + } + } + } + + static class InputContentType { + int imeOptions = EditorInfo.IME_NULL; + String privateImeOptions; + CharSequence imeActionLabel; + int imeActionId; + Bundle extras; + OnEditorActionListener onEditorActionListener; + boolean enterDown; + } + + static class InputMethodState { + Rect mCursorRectInWindow = new Rect(); + RectF mTmpRectF = new RectF(); + float[] mTmpOffset = new float[2]; + ExtractedTextRequest mExtracting; + final ExtractedText mTmpExtracted = new ExtractedText(); + int mBatchEditNesting; + boolean mCursorChanged; + boolean mSelectionModeChanged; + boolean mContentChanged; + int mChangedStart, mChangedEnd, mChangedDelta; + } +} diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 4bdb3e284cba..2a81f080eeb8 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -18,11 +18,8 @@ package android.widget; import android.R; import android.content.ClipData; -import android.content.ClipData.Item; import android.content.ClipboardManager; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.CompatibilityInfo; import android.content.res.Resources; @@ -43,7 +40,6 @@ import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; -import android.provider.Settings; import android.text.BoringLayout; import android.text.DynamicLayout; import android.text.Editable; @@ -57,7 +53,6 @@ import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableString; -import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; @@ -86,42 +81,31 @@ import android.text.method.TransformationMethod2; import android.text.method.WordIterator; import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; -import android.text.style.EasyEditSpan; import android.text.style.ParagraphStyle; import android.text.style.SpellCheckSpan; -import android.text.style.SuggestionRangeSpan; import android.text.style.SuggestionSpan; -import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.util.FloatMath; import android.util.Log; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ActionMode.Callback; -import android.view.DisplayList; import android.view.DragEvent; import android.view.Gravity; import android.view.HapticFeedbackConstants; -import android.view.HardwareCanvas; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewDebug; -import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; -import android.view.ViewParent; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; -import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; @@ -136,10 +120,8 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.textservice.SpellCheckerSubtype; import android.view.textservice.TextServicesManager; -import android.widget.AdapterView.OnItemClickListener; import android.widget.RemoteViews.RemoteView; -import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastMath; import com.android.internal.widget.EditableInputConnection; @@ -147,11 +129,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.lang.ref.WeakReference; -import java.text.BreakIterator; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; import java.util.Locale; /** @@ -267,24 +245,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private static final int PIXELS = 2; private static final RectF TEMP_RECTF = new RectF(); - private static final float[] TEMP_POSITION = new float[2]; // XXX should be much larger private static final int VERY_WIDE = 1024*1024; - private static final int BLINK = 500; private static final int ANIMATED_SCROLL_GAP = 250; private static final InputFilter[] NO_FILTERS = new InputFilter[0]; private static final Spanned EMPTY_SPANNED = new SpannedString(""); - private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; private static final int CHANGE_WATCHER_PRIORITY = 100; // New state used to change background based on whether this TextView is multiline. private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline }; // System wide time for last cut or copy action. - private static long LAST_CUT_OR_COPY_TIME; + static long LAST_CUT_OR_COPY_TIME; private int mCurrentAlpha = 255; @@ -316,7 +291,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mDrawableHeightStart, mDrawableHeightEnd; int mDrawablePadding; } - private Drawables mDrawables; + Drawables mDrawables; private CharWrapper mCharWrapper; @@ -404,23 +379,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // It is possible to have a selection even when mEditor is null (programmatically set, like when // a link is pressed). These highlight-related fields do not go in mEditor. - private int mHighlightColor = 0x6633B5E5; + int mHighlightColor = 0x6633B5E5; private Path mHighlightPath; private final Paint mHighlightPaint; private boolean mHighlightPathBogus = true; // Although these fields are specific to editable text, they are not added to Editor because // they are defined by the TextView's style and are theme-dependent. - private int mCursorDrawableRes; + int mCursorDrawableRes; // These four fields, could be moved to Editor, since we know their default values and we // could condition the creation of the Editor to a non standard value. This is however // brittle since the hardcoded values here (such as // com.android.internal.R.drawable.text_select_handle_left) would have to be updated if the // default style is modified. - private int mTextSelectHandleLeftRes; - private int mTextSelectHandleRightRes; - private int mTextSelectHandleRes; - private int mTextEditSuggestionItemLayout; + int mTextSelectHandleLeftRes; + int mTextSelectHandleRightRes; + int mTextSelectHandleRes; + int mTextEditSuggestionItemLayout; /** * EditText specific data, created on demand when one of the Editor fields is used. @@ -826,26 +801,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_imeOptions: createEditorIfNeeded("IME options specified in constructor"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeOptions = a.getInt(attr, getEditor().mInputContentType.imeOptions); break; case com.android.internal.R.styleable.TextView_imeActionLabel: createEditorIfNeeded("IME action label specified in constructor"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeActionLabel = a.getText(attr); break; case com.android.internal.R.styleable.TextView_imeActionId: createEditorIfNeeded("IME action id specified in constructor"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeActionId = a.getInt(attr, getEditor().mInputContentType.imeActionId); break; @@ -1135,7 +1104,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setClickable(clickable); setLongClickable(longClickable); - prepareCursorControllers(); + if (mEditor != null) mEditor.prepareCursorControllers(); } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { @@ -1216,11 +1185,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Will change text color - if (mEditor != null) getEditor().invalidateTextDisplayList(); - prepareCursorControllers(); + if (mEditor != null) { + getEditor().invalidateTextDisplayList(); + getEditor().prepareCursorControllers(); - // start or stop the cursor blinking as appropriate - makeBlink(); + // start or stop the cursor blinking as appropriate + getEditor().makeBlink(); + } } /** @@ -1412,7 +1383,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener fixFocusableAndClickableSettings(); // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } } @@ -3250,7 +3221,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // SelectionModifierCursorController depends on textCanBeSelected, which depends on text - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } /** @@ -3366,12 +3337,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mHint; } + boolean isSingleLine() { + return mSingleLine; + } + private static boolean isMultilineInputType(int type) { return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); } /** + * Removes the suggestion spans. + */ + CharSequence removeSuggestionSpans(CharSequence text) { + if (text instanceof Spanned) { + Spannable spannable; + if (text instanceof Spannable) { + spannable = (Spannable) text; + } else { + spannable = new SpannableString(text); + text = spannable; + } + + SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class); + for (int i = 0; i < spans.length; i++) { + spannable.removeSpan(spans[i]); + } + } + return text; + } + + /** * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)}, * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL} @@ -3543,9 +3539,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setImeOptions(int imeOptions) { createEditorIfNeeded("IME options specified"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeOptions = imeOptions; } @@ -3572,9 +3566,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setImeActionLabel(CharSequence label, int actionId) { createEditorIfNeeded("IME action label specified"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeActionLabel = label; getEditor().mInputContentType.imeActionId = actionId; } @@ -3611,9 +3603,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setOnEditorActionListener(OnEditorActionListener l) { createEditorIfNeeded("Editor action listener set"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.onEditorActionListener = l; } @@ -3638,7 +3628,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @see #setOnEditorActionListener */ public void onEditorAction(int actionCode) { - final InputContentType ict = mEditor == null ? null : getEditor().mInputContentType; + final Editor.InputContentType ict = mEditor == null ? null : getEditor().mInputContentType; if (ict != null) { if (ict.onEditorActionListener != null) { if (ict.onEditorActionListener.onEditorAction(this, @@ -3710,8 +3700,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setPrivateImeOptions(String type) { createEditorIfNeeded("Private IME option set"); - if (getEditor().mInputContentType == null) - getEditor().mInputContentType = new InputContentType(); + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.privateImeOptions = type; } @@ -3740,8 +3729,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void setInputExtras(int xmlResId) throws XmlPullParserException, IOException { createEditorIfNeeded("Input extra set"); XmlResourceParser parser = getResources().getXml(xmlResId); - if (getEditor().mInputContentType == null) - getEditor().mInputContentType = new InputContentType(); + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.extras = new Bundle(); getResources().parseBundleExtras(parser, getEditor().mInputContentType.extras); } @@ -3761,7 +3749,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener createEditorIfNeeded("get Input extra"); if (getEditor().mInputContentType == null) { if (!create) return null; - getEditor().mInputContentType = new InputContentType(); + getEditor().createInputContentTypeIfNeeded(); } if (getEditor().mInputContentType.extras == null) { if (!create) return null; @@ -3811,142 +3799,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setError(CharSequence error, Drawable icon) { createEditorIfNeeded("setError"); - error = TextUtils.stringOrSpannedString(error); - - getEditor().mError = error; - getEditor().mErrorWasChanged = true; - final Drawables dr = mDrawables; - if (dr != null) { - switch (getResolvedLayoutDirection()) { - default: - case LAYOUT_DIRECTION_LTR: - setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, - dr.mDrawableBottom); - break; - case LAYOUT_DIRECTION_RTL: - setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight, - dr.mDrawableBottom); - break; - } - } else { - setCompoundDrawables(null, null, icon, null); - } - - if (error == null) { - if (getEditor().mErrorPopup != null) { - if (getEditor().mErrorPopup.isShowing()) { - getEditor().mErrorPopup.dismiss(); - } - - getEditor().mErrorPopup = null; - } - } else { - if (isFocused()) { - showError(); - } - } - } - - private void showError() { - if (getWindowToken() == null) { - getEditor().mShowErrorAfterAttach = true; - return; - } - - if (getEditor().mErrorPopup == null) { - LayoutInflater inflater = LayoutInflater.from(getContext()); - final TextView err = (TextView) inflater.inflate( - com.android.internal.R.layout.textview_hint, null); - - final float scale = getResources().getDisplayMetrics().density; - getEditor().mErrorPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f)); - getEditor().mErrorPopup.setFocusable(false); - // The user is entering text, so the input method is needed. We - // don't want the popup to be displayed on top of it. - getEditor().mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - } - - TextView tv = (TextView) getEditor().mErrorPopup.getContentView(); - chooseSize(getEditor().mErrorPopup, getEditor().mError, tv); - tv.setText(getEditor().mError); - - getEditor().mErrorPopup.showAsDropDown(this, getErrorX(), getErrorY()); - getEditor().mErrorPopup.fixDirection(getEditor().mErrorPopup.isAboveAnchor()); - } - - /** - * Returns the Y offset to make the pointy top of the error point - * at the middle of the error icon. - */ - private int getErrorX() { - /* - * The "25" is the distance between the point and the right edge - * of the background - */ - final float scale = getResources().getDisplayMetrics().density; - - final Drawables dr = mDrawables; - return getWidth() - getEditor().mErrorPopup.getWidth() - getPaddingRight() - - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); - } - - /** - * Returns the Y offset to make the pointy top of the error point - * at the bottom of the error icon. - */ - private int getErrorY() { - /* - * Compound, not extended, because the icon is not clipped - * if the text height is smaller. - */ - final int compoundPaddingTop = getCompoundPaddingTop(); - int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop; - - final Drawables dr = mDrawables; - int icontop = compoundPaddingTop + - (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; - - /* - * The "2" is the distance between the point and the top edge - * of the background. - */ - final float scale = getResources().getDisplayMetrics().density; - return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() - - (int) (2 * scale + 0.5f); - } - - private void hideError() { - if (getEditor().mErrorPopup != null) { - if (getEditor().mErrorPopup.isShowing()) { - getEditor().mErrorPopup.dismiss(); - } - } - - getEditor().mShowErrorAfterAttach = false; - } - - private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { - int wid = tv.getPaddingLeft() + tv.getPaddingRight(); - int ht = tv.getPaddingTop() + tv.getPaddingBottom(); - - int defaultWidthInPixels = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.textview_error_popup_default_width); - Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels, - Layout.Alignment.ALIGN_NORMAL, 1, 0, true); - float max = 0; - for (int i = 0; i < l.getLineCount(); i++) { - max = Math.max(max, l.getLineWidth(i)); - } - - /* - * Now set the popup size to be big enough for the text plus the border capped - * to DEFAULT_MAX_POPUP_WIDTH - */ - pop.setWidth(wid + (int) Math.ceil(max)); - pop.setHeight(ht + l.getHeight()); + getEditor().setError(error, icon); } - @Override protected boolean setFrame(int l, int t, int r, int b) { boolean result = super.setFrame(l, t, r, b); @@ -4009,7 +3864,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ///////////////////////////////////////////////////////////////////////// - private int getVerticalOffset(boolean forceNormal) { + int getVerticalOffset(boolean forceNormal) { int voffset = 0; final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; @@ -4071,7 +3926,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return voffset; } - private void invalidateCursorPath() { + void invalidateCursorPath() { if (mHighlightPathBogus) { invalidateCursor(); } else { @@ -4114,7 +3969,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void invalidateCursor() { + void invalidateCursor() { int where = getSelectionEnd(); invalidateCursor(where, where, where); @@ -4130,8 +3985,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Invalidates the region of text enclosed between the start and end text offsets. - * - * @hide */ void invalidateRegion(int start, int end, boolean invalidateCursor) { if (mLayout == null) { @@ -4237,15 +4090,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // - onFocusChanged cannot start it when focus is given to a view with selected text (after // a screen rotation) since layout is not yet initialized at that point. if (mEditor != null && getEditor().mCreatedWithASelection) { - startSelectionActionMode(); + getEditor().startSelectionActionMode(); getEditor().mCreatedWithASelection = false; } // Phone specific code (there is no ExtractEditText on tablets). // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can // not be set. Do the test here instead. - if (this instanceof ExtractEditText && hasSelection()) { - startSelectionActionMode(); + if (this instanceof ExtractEditText && hasSelection() && mEditor != null) { + getEditor().startSelectionActionMode(); } getViewTreeObserver().removeOnPreDrawListener(this); @@ -4260,11 +4113,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTemporaryDetach = false; - if (mEditor != null && getEditor().mShowErrorAfterAttach) { - showError(); - getEditor().mShowErrorAfterAttach = false; - } - // Resolve drawables as the layout direction has been resolved resolveDrawables(); @@ -4495,7 +4343,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL); // Called by setText above, but safer in case of future code changes - prepareCursorControllers(); + getEditor().prepareCursorControllers(); } @Override @@ -4536,8 +4384,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int selEnd = getSelectionEnd(); if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) { if (selStart == selEnd) { - if (mEditor != null && isCursorVisible() && - (SystemClock.uptimeMillis() - getEditor().mShowCursor) % (2 * BLINK) < BLINK) { + if (mEditor != null && getEditor().isCursorVisible() && + (SystemClock.uptimeMillis() - getEditor().mShowCursor) % + (2 * Editor.BLINK) < Editor.BLINK) { if (mHighlightPathBogus) { if (mHighlightPath == null) mHighlightPath = new Path(); mHighlightPath.reset(); @@ -4730,14 +4579,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Path highlight = getUpdatedHighlightPath(); if (mEditor != null) { - getEditor().onDraw(canvas, layout, highlight, cursorOffsetVertical); + getEditor().onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical); } else { layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); + } - if (mMarquee != null && mMarquee.shouldDrawGhost()) { - canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); - layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); - } + if (mMarquee != null && mMarquee.shouldDrawGhost()) { + canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); + layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); } canvas.restore(); @@ -4853,7 +4702,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @hide - * @param offsetRequired */ @Override protected int getFadeTop(boolean offsetRequired) { @@ -4871,7 +4719,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @hide - * @param offsetRequired */ @Override protected int getFadeHeight(boolean offsetRequired) { @@ -5252,9 +5099,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (onCheckIsTextEditor() && isEnabled()) { - if (getEditor().mInputMethodState == null) { - getEditor().mInputMethodState = new InputMethodState(); - } + getEditor().createInputMethodStateIfNeeded(); outAttrs.inputType = getInputType(); if (getEditor().mInputContentType != null) { outAttrs.imeOptions = getEditor().mInputContentType.imeOptions; @@ -5307,122 +5152,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * based on the information in <var>request</var> in to <var>outText</var>. * @return Returns true if the text was successfully extracted, else false. */ - public boolean extractText(ExtractedTextRequest request, - ExtractedText outText) { - return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, - EXTRACT_UNKNOWN, outText); + public boolean extractText(ExtractedTextRequest request, ExtractedText outText) { + createEditorIfNeeded("extractText"); + return getEditor().extractText(request, outText); } - - static final int EXTRACT_NOTHING = -2; - static final int EXTRACT_UNKNOWN = -1; - - boolean extractTextInternal(ExtractedTextRequest request, - int partialStartOffset, int partialEndOffset, int delta, - ExtractedText outText) { - final CharSequence content = mText; - if (content != null) { - if (partialStartOffset != EXTRACT_NOTHING) { - final int N = content.length(); - if (partialStartOffset < 0) { - outText.partialStartOffset = outText.partialEndOffset = -1; - partialStartOffset = 0; - partialEndOffset = N; - } else { - // Now use the delta to determine the actual amount of text - // we need. - partialEndOffset += delta; - // Adjust offsets to ensure we contain full spans. - if (content instanceof Spanned) { - Spanned spanned = (Spanned)content; - Object[] spans = spanned.getSpans(partialStartOffset, - partialEndOffset, ParcelableSpan.class); - int i = spans.length; - while (i > 0) { - i--; - int j = spanned.getSpanStart(spans[i]); - if (j < partialStartOffset) partialStartOffset = j; - j = spanned.getSpanEnd(spans[i]); - if (j > partialEndOffset) partialEndOffset = j; - } - } - outText.partialStartOffset = partialStartOffset; - outText.partialEndOffset = partialEndOffset - delta; - if (partialStartOffset > N) { - partialStartOffset = N; - } else if (partialStartOffset < 0) { - partialStartOffset = 0; - } - if (partialEndOffset > N) { - partialEndOffset = N; - } else if (partialEndOffset < 0) { - partialEndOffset = 0; - } - } - if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { - outText.text = content.subSequence(partialStartOffset, - partialEndOffset); - } else { - outText.text = TextUtils.substring(content, partialStartOffset, - partialEndOffset); - } - } else { - outText.partialStartOffset = 0; - outText.partialEndOffset = 0; - outText.text = ""; - } - outText.flags = 0; - if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { - outText.flags |= ExtractedText.FLAG_SELECTING; - } - if (mSingleLine) { - outText.flags |= ExtractedText.FLAG_SINGLE_LINE; - } - outText.startOffset = 0; - outText.selectionStart = getSelectionStart(); - outText.selectionEnd = getSelectionEnd(); - return true; - } - return false; - } - - boolean reportExtractedText() { - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null) { - final boolean contentChanged = ims.mContentChanged; - if (contentChanged || ims.mSelectionModeChanged) { - ims.mContentChanged = false; - ims.mSelectionModeChanged = false; - final ExtractedTextRequest req = ims.mExtracting; - if (req != null) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start=" - + ims.mChangedStart + " end=" + ims.mChangedEnd - + " delta=" + ims.mChangedDelta); - if (ims.mChangedStart < 0 && !contentChanged) { - ims.mChangedStart = EXTRACT_NOTHING; - } - if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, - ims.mChangedDelta, ims.mTmpExtracted)) { - if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start=" - + ims.mTmpExtracted.partialStartOffset - + " end=" + ims.mTmpExtracted.partialEndOffset - + ": " + ims.mTmpExtracted.text); - imm.updateExtractedText(this, req.token, ims.mTmpExtracted); - ims.mChangedStart = EXTRACT_UNKNOWN; - ims.mChangedEnd = EXTRACT_UNKNOWN; - ims.mChangedDelta = 0; - ims.mContentChanged = false; - return true; - } - } - } - } - } - return false; - } - /** * This is used to remove all style-impacting spans from text before new * extracted text is being replaced into it, so that we don't have any @@ -5436,7 +5170,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener spannable.removeSpan(spans[i]); } } - + /** * Apply to this text view the given extracted text, as previously * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}. @@ -5492,7 +5226,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // This would stop a possible selection mode, but no such mode is started in case // extracted mode will start. Some text is selected though, and will trigger an action mode // in the extracted view. - hideControllers(); + getEditor().hideControllers(); } /** @@ -5518,87 +5252,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @param info The auto correct info about the text that was corrected. */ public void onCommitCorrection(CorrectionInfo info) { - if (mEditor == null) return; - if (getEditor().mCorrectionHighlighter == null) { - getEditor().mCorrectionHighlighter = new CorrectionHighlighter(); - } else { - getEditor().mCorrectionHighlighter.invalidate(false); - } - - getEditor().mCorrectionHighlighter.highlight(info); + if (mEditor != null) getEditor().onCommitCorrection(info); } public void beginBatchEdit() { - if (mEditor == null) return; - getEditor().mInBatchEditControllers = true; - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null) { - int nesting = ++ims.mBatchEditNesting; - if (nesting == 1) { - ims.mCursorChanged = false; - ims.mChangedDelta = 0; - if (ims.mContentChanged) { - // We already have a pending change from somewhere else, - // so turn this into a full update. - ims.mChangedStart = 0; - ims.mChangedEnd = mText.length(); - } else { - ims.mChangedStart = EXTRACT_UNKNOWN; - ims.mChangedEnd = EXTRACT_UNKNOWN; - ims.mContentChanged = false; - } - onBeginBatchEdit(); - } - } + if (mEditor != null) getEditor().beginBatchEdit(); } public void endBatchEdit() { - if (mEditor == null) return; - getEditor().mInBatchEditControllers = false; - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null) { - int nesting = --ims.mBatchEditNesting; - if (nesting == 0) { - finishBatchEdit(ims); - } - } - } - - void ensureEndedBatchEdit() { - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null && ims.mBatchEditNesting != 0) { - ims.mBatchEditNesting = 0; - finishBatchEdit(ims); - } - } - - void finishBatchEdit(final InputMethodState ims) { - onEndBatchEdit(); - - if (ims.mContentChanged || ims.mSelectionModeChanged) { - updateAfterEdit(); - reportExtractedText(); - } else if (ims.mCursorChanged) { - // Cheezy way to get us to report the current cursor location. - invalidateCursor(); - } - } - - void updateAfterEdit() { - invalidate(); - int curs = getSelectionStart(); - - if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { - registerForPreDraw(); - } - - if (curs >= 0) { - mHighlightPathBogus = true; - makeBlink(); - bringPointIntoView(curs); - } - - checkForResize(); + if (mEditor != null) getEditor().endBatchEdit(); } /** @@ -5644,7 +5306,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mBoring = mHintBoring = null; // Since it depends on the value of mLayout - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } /** @@ -5826,7 +5488,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // CursorControllers need a non-null mLayout - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, @@ -6638,11 +6300,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener r.bottom += verticalOffset; } - private int viewportToContentHorizontalOffset() { + int viewportToContentHorizontalOffset() { return getCompoundPaddingLeft() - mScrollX; } - private int viewportToContentVerticalOffset() { + int viewportToContentVerticalOffset() { int offset = getExtendedPaddingTop() - mScrollY; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { offset += getVerticalOffset(false); @@ -6855,18 +6517,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener getEditor().mCursorVisible = visible; invalidate(); - makeBlink(); + getEditor().makeBlink(); // InsertionPointCursorController depends on mCursorVisible - prepareCursorControllers(); + getEditor().prepareCursorControllers(); } } - private boolean isCursorVisible() { - // The default value is true, even when there is no associated Editor - return mEditor == null ? true : (getEditor().mCursorVisible && isTextEditable()); - } - private boolean canMarquee() { int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()); return width > 0 && (mLayout.getLineWidth(0) > width || @@ -7049,12 +6706,29 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + void updateAfterEdit() { + invalidate(); + int curs = getSelectionStart(); + + if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { + registerForPreDraw(); + } + + if (curs >= 0) { + mHighlightPathBogus = true; + if (mEditor != null) getEditor().makeBlink(); + bringPointIntoView(curs); + } + + checkForResize(); + } + /** * Not private so it can be called from an inner class without going * through a thunk. */ void handleTextChanged(CharSequence buffer, int start, int before, int after) { - final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; + final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; if (ims == null || ims.mBatchEditNesting == 0) { updateAfterEdit(); } @@ -7085,7 +6759,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean selChanged = false; int newSelStart=-1, newSelEnd=-1; - final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; + final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; if (what == Selection.SELECTION_END) { selChanged = true; @@ -7094,7 +6768,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (oldStart >= 0 || newStart >= 0) { invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart); registerForPreDraw(); - makeBlink(); + if (mEditor != null) getEditor().makeBlink(); } } @@ -7186,20 +6860,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Create new SpellCheckSpans on the modified region. - */ - private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) { - if (isTextEditable() && isSuggestionsEnabled() && !(this instanceof ExtractEditText)) { - if (getEditor().mSpellChecker == null && createSpellChecker) { - getEditor().mSpellChecker = new SpellChecker(this); - } - if (getEditor().mSpellChecker != null) { - getEditor().mSpellChecker.spellCheck(start, end); - } - } - } - - /** * @hide */ @Override @@ -7219,7 +6879,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Because of View recycling in ListView, there is no easy way to know when a TextView with // selection becomes visible again. Until a better solution is found, stop text selection // mode (if any) as soon as this TextView is recycled. - if (mEditor != null) hideControllers(); + if (mEditor != null) getEditor().hideControllers(); } @Override @@ -7269,7 +6929,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (mEditor != null && visibility != VISIBLE) { - hideControllers(); + getEditor().hideControllers(); } } @@ -7350,31 +7010,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener handled |= imm != null && imm.showSoftInput(this, 0); } - boolean selectAllGotFocus = getEditor().mSelectAllOnFocus && didTouchFocusSelect(); - hideControllers(); - if (!selectAllGotFocus && mText.length() > 0) { - // Move cursor - final int offset = getOffsetForPosition(event.getX(), event.getY()); - Selection.setSelection((Spannable) mText, offset); - if (getEditor().mSpellChecker != null) { - // When the cursor moves, the word that was typed may need spell check - getEditor().mSpellChecker.onSelectionChanged(); - } - if (!extractedTextModeWillBeStarted()) { - if (isCursorInsideEasyCorrectionSpan()) { - getEditor().mShowSuggestionRunnable = new Runnable() { - public void run() { - showSuggestions(); - } - }; - // removeCallbacks is performed on every touch - postDelayed(getEditor().mShowSuggestionRunnable, - ViewConfiguration.getDoubleTapTimeout()); - } else if (hasInsertionController()) { - getInsertionController().show(); - } - } - } + // The above condition ensures that the mEditor is not null + getEditor().onTouchUpEvent(event); handled = true; } @@ -7387,53 +7024,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } - /** - * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}. - */ - private boolean isCursorInsideSuggestionSpan() { - if (!(mText instanceof Spannable)) return false; - - SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(), - getSelectionEnd(), SuggestionSpan.class); - return (suggestionSpans.length > 0); - } - - /** - * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with - * {@link SuggestionSpan#FLAG_EASY_CORRECT} set. - */ - private boolean isCursorInsideEasyCorrectionSpan() { - Spannable spannable = (Spannable) mText; - SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(), - getSelectionEnd(), SuggestionSpan.class); - for (int i = 0; i < suggestionSpans.length; i++) { - if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) { - return true; - } - } - return false; - } - - /** - * Downgrades to simple suggestions all the easy correction spans that are not a spell check - * span. - */ - private void downgradeEasyCorrectionSpans() { - if (mText instanceof Spannable) { - Spannable spannable = (Spannable) mText; - SuggestionSpan[] suggestionSpans = spannable.getSpans(0, - spannable.length(), SuggestionSpan.class); - for (int i = 0; i < suggestionSpans.length; i++) { - int flags = suggestionSpans[i].getFlags(); - if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0 - && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) { - flags &= ~SuggestionSpan.FLAG_EASY_CORRECT; - suggestionSpans[i].setFlags(flags); - } - } - } - } - @Override public boolean onGenericMotionEvent(MotionEvent event) { if (mMovement != null && mText instanceof Spannable && mLayout != null) { @@ -7450,44 +7040,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onGenericMotionEvent(event); } - private void prepareCursorControllers() { - if (mEditor == null) return; - - boolean windowSupportsHandles = false; - - ViewGroup.LayoutParams params = getRootView().getLayoutParams(); - if (params instanceof WindowManager.LayoutParams) { - WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params; - windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW - || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW; - } - - getEditor().mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null; - getEditor().mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() && - mLayout != null; - - if (!getEditor().mInsertionControllerEnabled) { - hideInsertionPointCursorController(); - if (getEditor().mInsertionPointCursorController != null) { - getEditor().mInsertionPointCursorController.onDetached(); - getEditor().mInsertionPointCursorController = null; - } - } - - if (!getEditor().mSelectionControllerEnabled) { - stopSelectionActionMode(); - if (getEditor().mSelectionModifierCursorController != null) { - getEditor().mSelectionModifierCursorController.onDetached(); - getEditor().mSelectionModifierCursorController = null; - } - } - } - /** * @return True iff this TextView contains a text that can be edited, or if this is * a selectable TextView. */ - private boolean isTextEditable() { + boolean isTextEditable() { return mText instanceof Editable && onCheckIsTextEditor() && isEnabled(); } @@ -7522,32 +7079,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mScroller = s; } - /** - * @return True when the TextView isFocused and has a valid zero-length selection (cursor). - */ - private boolean shouldBlink() { - if (mEditor == null || !isCursorVisible() || !isFocused()) return false; - - final int start = getSelectionStart(); - if (start < 0) return false; - - final int end = getSelectionEnd(); - if (end < 0) return false; - - return start == end; - } - - private void makeBlink() { - if (shouldBlink()) { - getEditor().mShowCursor = SystemClock.uptimeMillis(); - if (getEditor().mBlink == null) getEditor().mBlink = new Blink(this); - getEditor().mBlink.removeCallbacks(getEditor().mBlink); - getEditor().mBlink.postAtTime(getEditor().mBlink, getEditor().mShowCursor + BLINK); - } else { - if (mEditor != null && getEditor().mBlink != null) getEditor().mBlink.removeCallbacks(getEditor().mBlink); - } - } - @Override protected float getLeftFadingEdgeStrength() { if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f; @@ -7726,10 +7257,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have - * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient. + * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not sufficient. */ private boolean canSelectText() { - return hasSelectionController() && mText.length() != 0; + return mText.length() != 0 && mEditor != null && getEditor().hasSelectionController(); } /** @@ -7738,7 +7269,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * See also {@link #canSelectText()}. */ - private boolean textCanBeSelected() { + boolean textCanBeSelected() { // prepareCursorController() relies on this method. // If you change this condition, make sure prepareCursorController is called anywhere // the value of this condition might be changed. @@ -7746,112 +7277,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return isTextEditable() || (isTextSelectable() && mText instanceof Spannable && isEnabled()); } - private boolean canCut() { - if (hasPasswordTransformationMethod()) { - return false; - } - - if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) { - return true; - } - - return false; - } - - private boolean canCopy() { - if (hasPasswordTransformationMethod()) { - return false; - } - - if (mText.length() > 0 && hasSelection()) { - return true; - } - - return false; - } - - private boolean canPaste() { - return (mText instanceof Editable && - mEditor != null && getEditor().mKeyListener != null && - getSelectionStart() >= 0 && - getSelectionEnd() >= 0 && - ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)). - hasPrimaryClip()); - } - - private boolean selectAll() { - final int length = mText.length(); - Selection.setSelection((Spannable) mText, 0, length); - return length > 0; - } - - /** - * Adjusts selection to the word under last touch offset. - * Return true if the operation was successfully performed. - */ - private boolean selectCurrentWord() { - if (!canSelectText()) { - return false; - } - - if (hasPasswordTransformationMethod()) { - // Always select all on a password field. - // Cut/copy menu entries are not available for passwords, but being able to select all - // is however useful to delete or paste to replace the entire content. - return selectAll(); - } - - int inputType = getInputType(); - int klass = inputType & InputType.TYPE_MASK_CLASS; - int variation = inputType & InputType.TYPE_MASK_VARIATION; - - // Specific text field types: select the entire text for these - if (klass == InputType.TYPE_CLASS_NUMBER || - klass == InputType.TYPE_CLASS_PHONE || - klass == InputType.TYPE_CLASS_DATETIME || - variation == InputType.TYPE_TEXT_VARIATION_URI || - variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - return selectAll(); - } - - long lastTouchOffsets = getLastTouchOffsets(); - final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets); - final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets); - - // Safety check in case standard touch event handling has been bypassed - if (minOffset < 0 || minOffset >= mText.length()) return false; - if (maxOffset < 0 || maxOffset >= mText.length()) return false; - - int selectionStart, selectionEnd; - - // If a URLSpan (web address, email, phone...) is found at that position, select it. - URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class); - if (urlSpans.length >= 1) { - URLSpan urlSpan = urlSpans[0]; - selectionStart = ((Spanned) mText).getSpanStart(urlSpan); - selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan); - } else { - final WordIterator wordIterator = getWordIterator(); - wordIterator.setCharSequence(mText, minOffset, maxOffset); - - selectionStart = wordIterator.getBeginning(minOffset); - selectionEnd = wordIterator.getEnd(maxOffset); - - if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE || - selectionStart == selectionEnd) { - // Possible when the word iterator does not properly handle the text's language - long range = getCharRange(minOffset); - selectionStart = TextUtils.unpackRangeStartFromLong(range); - selectionEnd = TextUtils.unpackRangeEndFromLong(range); - } - } - - Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); - return selectionEnd > selectionStart; - } - /** * This is a temporary method. Future versions may support multi-locale text. * @@ -7877,45 +7302,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * This method is used by the ArrowKeyMovementMethod to jump from one word to the other. + * Made available to achieve a consistent behavior. * @hide */ public WordIterator getWordIterator() { - if (getEditor().mWordIterator == null) { - getEditor().mWordIterator = new WordIterator(getTextServicesLocale()); - } - return getEditor().mWordIterator; - } - - private long getCharRange(int offset) { - final int textLength = mText.length(); - if (offset + 1 < textLength) { - final char currentChar = mText.charAt(offset); - final char nextChar = mText.charAt(offset + 1); - if (Character.isSurrogatePair(currentChar, nextChar)) { - return TextUtils.packRangeInLong(offset, offset + 2); - } - } - if (offset < textLength) { - return TextUtils.packRangeInLong(offset, offset + 1); - } - if (offset - 2 >= 0) { - final char previousChar = mText.charAt(offset - 1); - final char previousPreviousChar = mText.charAt(offset - 2); - if (Character.isSurrogatePair(previousPreviousChar, previousChar)) { - return TextUtils.packRangeInLong(offset - 2, offset); - } - } - if (offset - 1 >= 0) { - return TextUtils.packRangeInLong(offset - 1, offset); + if (getEditor() != null) { + return mEditor.getWordIterator(); + } else { + return null; } - return TextUtils.packRangeInLong(offset, offset); - } - - private long getLastTouchOffsets() { - SelectionModifierCursorController selectionController = getSelectionController(); - final int minOffset = selectionController.getMinTouchOffset(); - final int maxOffset = selectionController.getMaxTouchOffset(); - return TextUtils.packRangeInLong(minOffset, maxOffset); } @Override @@ -8004,11 +7400,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return imm != null && imm.isActive(this); } - // Selection context mode - private static final int ID_SELECT_ALL = android.R.id.selectAll; - private static final int ID_CUT = android.R.id.cut; - private static final int ID_COPY = android.R.id.copy; - private static final int ID_PASTE = android.R.id.paste; + static final int ID_SELECT_ALL = android.R.id.selectAll; + static final int ID_CUT = android.R.id.cut; + static final int ID_COPY = android.R.id.copy; + static final int ID_PASTE = android.R.id.paste; /** * Called when a context menu option for the text view is selected. Currently @@ -8033,7 +7428,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case ID_SELECT_ALL: // This does not enter text selection mode. Text is highlighted, so that it can be // bulk edited, like selectAllOnFocus does. Returns true even if text is empty. - selectAll(); + selectAllText(); return true; case ID_PASTE: @@ -8054,89 +7449,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - private CharSequence getTransformedText(int start, int end) { + CharSequence getTransformedText(int start, int end) { return removeSuggestionSpans(mTransformed.subSequence(start, end)); } - /** - * Prepare text so that there are not zero or two spaces at beginning and end of region defined - * by [min, max] when replacing this region by paste. - * Note that if there were two spaces (or more) at that position before, they are kept. We just - * make sure we do not add an extra one from the paste content. - */ - private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) { - if (paste.length() > 0) { - if (min > 0) { - final char charBefore = mTransformed.charAt(min - 1); - final char charAfter = paste.charAt(0); - - if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { - // Two spaces at beginning of paste: remove one - final int originalLength = mText.length(); - deleteText_internal(min - 1, min); - // Due to filters, there is no guarantee that exactly one character was - // removed: count instead. - final int delta = mText.length() - originalLength; - min += delta; - max += delta; - } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && - !Character.isSpaceChar(charAfter) && charAfter != '\n') { - // No space at beginning of paste: add one - final int originalLength = mText.length(); - replaceText_internal(min, min, " "); - // Taking possible filters into account as above. - final int delta = mText.length() - originalLength; - min += delta; - max += delta; - } - } - - if (max < mText.length()) { - final char charBefore = paste.charAt(paste.length() - 1); - final char charAfter = mTransformed.charAt(max); - - if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { - // Two spaces at end of paste: remove one - deleteText_internal(max, max + 1); - } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && - !Character.isSpaceChar(charAfter) && charAfter != '\n') { - // No space at end of paste: add one - replaceText_internal(max, max, " "); - } - } - } - - return TextUtils.packRangeInLong(min, max); - } - - private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) { - TextView shadowView = (TextView) inflate(mContext, - com.android.internal.R.layout.text_drag_thumbnail, null); - - if (shadowView == null) { - throw new IllegalArgumentException("Unable to inflate text drag thumbnail"); - } - - if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) { - text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH); - } - shadowView.setText(text); - shadowView.setTextColor(getTextColors()); - - shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge); - shadowView.setGravity(Gravity.CENTER); - - shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - - final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - shadowView.measure(size, size); - - shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight()); - shadowView.invalidate(); - return new DragShadowBuilder(shadowView); - } - @Override public boolean performLongClick() { boolean handled = false; @@ -8145,179 +7461,27 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener handled = true; } - if (mEditor == null) { - return handled; - } - - // Long press in empty space moves cursor and shows the Paste affordance if available. - if (!handled && !isPositionOnText(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY) && - getEditor().mInsertionControllerEnabled) { - final int offset = getOffsetForPosition(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY); - stopSelectionActionMode(); - Selection.setSelection((Spannable) mText, offset); - getInsertionController().showWithActionPopup(); - handled = true; - } - - if (!handled && getEditor().mSelectionActionMode != null) { - if (touchPositionIsInSelection()) { - // Start a drag - final int start = getSelectionStart(); - final int end = getSelectionEnd(); - CharSequence selectedText = getTransformedText(start, end); - ClipData data = ClipData.newPlainText(null, selectedText); - DragLocalState localState = new DragLocalState(this, start, end); - startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); - stopSelectionActionMode(); - } else { - getSelectionController().hide(); - selectCurrentWord(); - getSelectionController().show(); - } - handled = true; - } - - // Start a new selection - if (!handled) { - handled = startSelectionActionMode(); + if (mEditor != null) { + handled |= getEditor().performLongClick(handled); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - getEditor().mDiscardNextActionUp = true; + if (mEditor != null) getEditor().mDiscardNextActionUp = true; } return handled; } - private boolean touchPositionIsInSelection() { - int selectionStart = getSelectionStart(); - int selectionEnd = getSelectionEnd(); - - if (selectionStart == selectionEnd) { - return false; - } - - if (selectionStart > selectionEnd) { - int tmp = selectionStart; - selectionStart = selectionEnd; - selectionEnd = tmp; - Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); - } - - SelectionModifierCursorController selectionController = getSelectionController(); - int minOffset = selectionController.getMinTouchOffset(); - int maxOffset = selectionController.getMaxTouchOffset(); - - return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); - } - - private PositionListener getPositionListener() { - if (getEditor().mPositionListener == null) { - getEditor().mPositionListener = new PositionListener(); - } - return getEditor().mPositionListener; - } - - private interface TextViewPositionListener { - public void updatePosition(int parentPositionX, int parentPositionY, - boolean parentPositionChanged, boolean parentScrolled); - } - - private boolean isPositionVisible(int positionX, int positionY) { - synchronized (TEMP_POSITION) { - final float[] position = TEMP_POSITION; - position[0] = positionX; - position[1] = positionY; - View view = this; - - while (view != null) { - if (view != this) { - // Local scroll is already taken into account in positionX/Y - position[0] -= view.getScrollX(); - position[1] -= view.getScrollY(); - } - - if (position[0] < 0 || position[1] < 0 || - position[0] > view.getWidth() || position[1] > view.getHeight()) { - return false; - } - - if (!view.getMatrix().isIdentity()) { - view.getMatrix().mapPoints(position); - } - - position[0] += view.getLeft(); - position[1] += view.getTop(); - - final ViewParent parent = view.getParent(); - if (parent instanceof View) { - view = (View) parent; - } else { - // We've reached the ViewRoot, stop iterating - view = null; - } - } - } - - // We've been able to walk up the view hierarchy and the position was never clipped - return true; - } - - private boolean isOffsetVisible(int offset) { - final int line = mLayout.getLineForOffset(offset); - final int lineBottom = mLayout.getLineBottom(line); - final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset); - return isPositionVisible(primaryHorizontal + viewportToContentHorizontalOffset(), - lineBottom + viewportToContentVerticalOffset()); - } - @Override protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) { super.onScrollChanged(horiz, vert, oldHoriz, oldVert); if (mEditor != null) { - if (getEditor().mPositionListener != null) { - getEditor().mPositionListener.onScrollChanged(); - } - // Internal scroll affects the clip boundaries - getEditor().invalidateTextDisplayList(); + getEditor().onScrollChanged(); } } /** - * Removes the suggestion spans. - */ - CharSequence removeSuggestionSpans(CharSequence text) { - if (text instanceof Spanned) { - Spannable spannable; - if (text instanceof Spannable) { - spannable = (Spannable) text; - } else { - spannable = new SpannableString(text); - text = spannable; - } - - SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class); - for (int i = 0; i < spans.length; i++) { - spannable.removeSpan(spans[i]); - } - } - return text; - } - - void showSuggestions() { - if (getEditor().mSuggestionsPopupWindow == null) { - getEditor().mSuggestionsPopupWindow = new SuggestionsPopupWindow(); - } - hideControllers(); - getEditor().mSuggestionsPopupWindow.show(); - } - - boolean areSuggestionsShown() { - return getEditor().mSuggestionsPopupWindow != null && getEditor().mSuggestionsPopupWindow.isShowing(); - } - - /** * Return whether or not suggestions are enabled on this TextView. The suggestions are generated * by the IME or by the spell checker as the user types. This is done by adding * {@link SuggestionSpan}s to the text. @@ -8391,65 +7555,100 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * - * @return true if the selection mode was actually started. + * @hide */ - private boolean startSelectionActionMode() { - if (getEditor().mSelectionActionMode != null) { - // Selection action mode is already started - return false; - } + protected void stopSelectionActionMode() { + getEditor().stopSelectionActionMode(); + } - if (!canSelectText() || !requestFocus()) { - Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled."); + boolean canCut() { + if (hasPasswordTransformationMethod()) { return false; } - if (!hasSelection()) { - // There may already be a selection on device rotation - if (!selectCurrentWord()) { - // No word found under cursor or text selection not permitted. - return false; - } + if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) { + return true; } - boolean willExtract = extractedTextModeWillBeStarted(); + return false; + } - // Do not start the action mode when extracted text will show up full screen, which would - // immediately hide the newly created action bar and would be visually distracting. - if (!willExtract) { - ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); - getEditor().mSelectionActionMode = startActionMode(actionModeCallback); + boolean canCopy() { + if (hasPasswordTransformationMethod()) { + return false; } - final boolean selectionStarted = getEditor().mSelectionActionMode != null || willExtract; - if (selectionStarted && !isTextSelectable()) { - // Show the IME to be able to replace text, except when selecting non editable text. - final InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - imm.showSoftInput(this, 0, null); - } + if (mText.length() > 0 && hasSelection()) { + return true; } - return selectionStarted; + return false; } - private boolean extractedTextModeWillBeStarted() { - if (!(this instanceof ExtractEditText)) { - final InputMethodManager imm = InputMethodManager.peekInstance(); - return imm != null && imm.isFullscreenMode(); - } - return false; + boolean canPaste() { + return (mText instanceof Editable && + mEditor != null && getEditor().mKeyListener != null && + getSelectionStart() >= 0 && + getSelectionEnd() >= 0 && + ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)). + hasPrimaryClip()); + } + + boolean selectAllText() { + final int length = mText.length(); + Selection.setSelection((Spannable) mText, 0, length); + return length > 0; } /** - * @hide + * Prepare text so that there are not zero or two spaces at beginning and end of region defined + * by [min, max] when replacing this region by paste. + * Note that if there were two spaces (or more) at that position before, they are kept. We just + * make sure we do not add an extra one from the paste content. */ - protected void stopSelectionActionMode() { - if (getEditor().mSelectionActionMode != null) { - // This will hide the mSelectionModifierCursorController - getEditor().mSelectionActionMode.finish(); + long prepareSpacesAroundPaste(int min, int max, CharSequence paste) { + if (paste.length() > 0) { + if (min > 0) { + final char charBefore = mTransformed.charAt(min - 1); + final char charAfter = paste.charAt(0); + + if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { + // Two spaces at beginning of paste: remove one + final int originalLength = mText.length(); + deleteText_internal(min - 1, min); + // Due to filters, there is no guarantee that exactly one character was + // removed: count instead. + final int delta = mText.length() - originalLength; + min += delta; + max += delta; + } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && + !Character.isSpaceChar(charAfter) && charAfter != '\n') { + // No space at beginning of paste: add one + final int originalLength = mText.length(); + replaceText_internal(min, min, " "); + // Taking possible filters into account as above. + final int delta = mText.length() - originalLength; + min += delta; + max += delta; + } + } + + if (max < mText.length()) { + final char charBefore = paste.charAt(paste.length() - 1); + final char charAfter = mTransformed.charAt(max); + + if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { + // Two spaces at end of paste: remove one + deleteText_internal(max, max + 1); + } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && + !Character.isSpaceChar(charAfter) && charAfter != '\n') { + // No space at end of paste: add one + replaceText_internal(max, max, " "); + } + } } + + return TextUtils.packRangeInLong(min, max); } /** @@ -8489,36 +7688,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener LAST_CUT_OR_COPY_TIME = SystemClock.uptimeMillis(); } - private void hideInsertionPointCursorController() { - // No need to create the controller to hide it. - if (getEditor().mInsertionPointCursorController != null) { - getEditor().mInsertionPointCursorController.hide(); - } - } - - /** - * Hides the insertion controller and stops text selection mode, hiding the selection controller - */ - private void hideControllers() { - hideCursorControllers(); - hideSpanControllers(); - } - - private void hideSpanControllers() { - if (mChangeWatcher != null) { - mChangeWatcher.hideControllers(); - } - } - - private void hideCursorControllers() { - if (getEditor().mSuggestionsPopupWindow != null && !getEditor().mSuggestionsPopupWindow.isShowingUp()) { - // Should be done before hide insertion point controller since it triggers a show of it - getEditor().mSuggestionsPopupWindow.hide(); - } - hideInsertionPointCursorController(); - stopSelectionActionMode(); - } - /** * Get the character offset closest to the specified absolute position. A typical use case is to * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method. @@ -8535,7 +7704,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return offset; } - private float convertToLocalHorizontalCoordinate(float x) { + float convertToLocalHorizontalCoordinate(float x) { x -= getTotalPaddingLeft(); // Clamp the position to inside of the view. x = Math.max(0.0f, x); @@ -8544,7 +7713,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return x; } - private int getLineAtCoordinate(float y) { + int getLineAtCoordinate(float y) { y -= getTotalPaddingTop(); // Clamp the position to inside of the view. y = Math.max(0.0f, y); @@ -8558,25 +7727,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return getLayout().getOffsetForHorizontal(line, x); } - /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed - * in the view. Returns false when the position is in the empty space of left/right of text. - */ - private boolean isPositionOnText(float x, float y) { - if (getLayout() == null) return false; - - final int line = getLineAtCoordinate(y); - x = convertToLocalHorizontalCoordinate(x); - - if (x < getLayout().getLineLeft(line)) return false; - if (x > getLayout().getLineRight(line)) return false; - return true; - } - @Override public boolean onDragEvent(DragEvent event) { switch (event.getAction()) { case DragEvent.ACTION_DRAG_STARTED: - return mEditor != null && hasInsertionController(); + return mEditor != null && getEditor().hasInsertionController(); case DragEvent.ACTION_DRAG_ENTERED: TextView.this.requestFocus(); @@ -8588,7 +7743,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; case DragEvent.ACTION_DROP: - onDrop(event); + if (mEditor != null) getEditor().onDrop(event); return true; case DragEvent.ACTION_DRAG_ENDED: @@ -8598,112 +7753,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void onDrop(DragEvent event) { - StringBuilder content = new StringBuilder(""); - ClipData clipData = event.getClipData(); - final int itemCount = clipData.getItemCount(); - for (int i=0; i < itemCount; i++) { - Item item = clipData.getItemAt(i); - content.append(item.coerceToText(TextView.this.mContext)); - } - - final int offset = getOffsetForPosition(event.getX(), event.getY()); - - Object localState = event.getLocalState(); - DragLocalState dragLocalState = null; - if (localState instanceof DragLocalState) { - dragLocalState = (DragLocalState) localState; - } - boolean dragDropIntoItself = dragLocalState != null && - dragLocalState.sourceTextView == this; - - if (dragDropIntoItself) { - if (offset >= dragLocalState.start && offset < dragLocalState.end) { - // A drop inside the original selection discards the drop. - return; - } - } - - final int originalLength = mText.length(); - long minMax = prepareSpacesAroundPaste(offset, offset, content); - int min = TextUtils.unpackRangeStartFromLong(minMax); - int max = TextUtils.unpackRangeEndFromLong(minMax); - - Selection.setSelection((Spannable) mText, max); - replaceText_internal(min, max, content); - - if (dragDropIntoItself) { - int dragSourceStart = dragLocalState.start; - int dragSourceEnd = dragLocalState.end; - if (max <= dragSourceStart) { - // Inserting text before selection has shifted positions - final int shift = mText.length() - originalLength; - dragSourceStart += shift; - dragSourceEnd += shift; - } - - // Delete original selection - deleteText_internal(dragSourceStart, dragSourceEnd); - - // Make sure we do not leave two adjacent spaces. - if ((dragSourceStart == 0 || - Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) && - (dragSourceStart == mText.length() || - Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) { - final int pos = dragSourceStart == mText.length() ? - dragSourceStart - 1 : dragSourceStart; - deleteText_internal(pos, pos + 1); - } - } - } - - /** - * @return True if this view supports insertion handles. - */ - boolean hasInsertionController() { - return getEditor().mInsertionControllerEnabled; - } - - /** - * @return True if this view supports selection handles. - */ - boolean hasSelectionController() { - return getEditor().mSelectionControllerEnabled; - } - - InsertionPointCursorController getInsertionController() { - if (!getEditor().mInsertionControllerEnabled) { - return null; - } - - if (getEditor().mInsertionPointCursorController == null) { - getEditor().mInsertionPointCursorController = new InsertionPointCursorController(); - - final ViewTreeObserver observer = getViewTreeObserver(); - observer.addOnTouchModeChangeListener(getEditor().mInsertionPointCursorController); - } - - return getEditor().mInsertionPointCursorController; - } - - SelectionModifierCursorController getSelectionController() { - if (!getEditor().mSelectionControllerEnabled) { - return null; - } - - if (getEditor().mSelectionModifierCursorController == null) { - getEditor().mSelectionModifierCursorController = new SelectionModifierCursorController(); - - final ViewTreeObserver observer = getViewTreeObserver(); - observer.addOnTouchModeChangeListener(getEditor().mSelectionModifierCursorController); - } - - return getEditor().mSelectionModifierCursorController; - } - boolean isInBatchEditMode() { if (mEditor == null) return false; - final InputMethodState ims = getEditor().mInputMethodState; + final Editor.InputMethodState ims = getEditor().mInputMethodState; if (ims != null) { return ims.mBatchEditNesting > 0; } @@ -8864,7 +7916,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (!(this instanceof EditText)) { Log.e(LOG_TAG + " EDITOR", "Creating Editor on TextView. " + reason); } - mEditor = new Editor(); + mEditor = new Editor(this); } else { if (!(this instanceof EditText)) { Log.d(LOG_TAG + " EDITOR", "Redundant Editor creation. " + reason); @@ -9041,151 +8093,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private static class ErrorPopup extends PopupWindow { - private boolean mAbove = false; - private final TextView mView; - private int mPopupInlineErrorBackgroundId = 0; - private int mPopupInlineErrorAboveBackgroundId = 0; - - ErrorPopup(TextView v, int width, int height) { - super(v, width, height); - mView = v; - // Make sure the TextView has a background set as it will be used the first time it is - // shown and positionned. Initialized with below background, which should have - // dimensions identical to the above version for this to work (and is more likely). - mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, - com.android.internal.R.styleable.Theme_errorMessageBackground); - mView.setBackgroundResource(mPopupInlineErrorBackgroundId); - } - - void fixDirection(boolean above) { - mAbove = above; - - if (above) { - mPopupInlineErrorAboveBackgroundId = - getResourceId(mPopupInlineErrorAboveBackgroundId, - com.android.internal.R.styleable.Theme_errorMessageAboveBackground); - } else { - mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, - com.android.internal.R.styleable.Theme_errorMessageBackground); - } - - mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId : - mPopupInlineErrorBackgroundId); - } - - private int getResourceId(int currentId, int index) { - if (currentId == 0) { - TypedArray styledAttributes = mView.getContext().obtainStyledAttributes( - R.styleable.Theme); - currentId = styledAttributes.getResourceId(index, 0); - styledAttributes.recycle(); - } - return currentId; - } - - @Override - public void update(int x, int y, int w, int h, boolean force) { - super.update(x, y, w, h, force); - - boolean above = isAboveAnchor(); - if (above != mAbove) { - fixDirection(above); - } - } - } - - private class CorrectionHighlighter { - private final Path mPath = new Path(); - private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private int mStart, mEnd; - private long mFadingStartTime; - private final static int FADE_OUT_DURATION = 400; - - public CorrectionHighlighter() { - mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale); - mPaint.setStyle(Paint.Style.FILL); - } - - public void highlight(CorrectionInfo info) { - mStart = info.getOffset(); - mEnd = mStart + info.getNewText().length(); - mFadingStartTime = SystemClock.uptimeMillis(); - - if (mStart < 0 || mEnd < 0) { - stopAnimation(); - } - } - - public void draw(Canvas canvas, int cursorOffsetVertical) { - if (updatePath() && updatePaint()) { - if (cursorOffsetVertical != 0) { - canvas.translate(0, cursorOffsetVertical); - } - - canvas.drawPath(mPath, mPaint); - - if (cursorOffsetVertical != 0) { - canvas.translate(0, -cursorOffsetVertical); - } - invalidate(true); // TODO invalidate cursor region only - } else { - stopAnimation(); - invalidate(false); // TODO invalidate cursor region only - } - } - - private boolean updatePaint() { - final long duration = SystemClock.uptimeMillis() - mFadingStartTime; - if (duration > FADE_OUT_DURATION) return false; - - final float coef = 1.0f - (float) duration / FADE_OUT_DURATION; - final int highlightColorAlpha = Color.alpha(mHighlightColor); - final int color = (mHighlightColor & 0x00FFFFFF) + - ((int) (highlightColorAlpha * coef) << 24); - mPaint.setColor(color); - return true; - } - - private boolean updatePath() { - final Layout layout = TextView.this.mLayout; - if (layout == null) return false; - - // Update in case text is edited while the animation is run - final int length = mText.length(); - int start = Math.min(length, mStart); - int end = Math.min(length, mEnd); - - mPath.reset(); - TextView.this.mLayout.getSelectionPath(start, end, mPath); - return true; - } - - private void invalidate(boolean delayed) { - if (TextView.this.mLayout == null) return; - - synchronized (TEMP_RECTF) { - mPath.computeBounds(TEMP_RECTF, false); - - int left = getCompoundPaddingLeft(); - int top = getExtendedPaddingTop() + getVerticalOffset(true); - - if (delayed) { - TextView.this.postInvalidateOnAnimation( - left + (int) TEMP_RECTF.left, top + (int) TEMP_RECTF.top, - left + (int) TEMP_RECTF.right, top + (int) TEMP_RECTF.bottom); - } else { - TextView.this.postInvalidate((int) TEMP_RECTF.left, (int) TEMP_RECTF.top, - (int) TEMP_RECTF.right, (int) TEMP_RECTF.bottom); - } - } - } - - private void stopAnimation() { - TextView.this.getEditor().mCorrectionHighlighter = null; - } - } - private static final class Marquee extends Handler { // TODO: Add an option to configure this private static final float MARQUEE_DELTA_MAX = 0.07f; @@ -9322,217 +8229,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - /** - * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related - * pop-up should be displayed. - */ - private class EasyEditSpanController { - - private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs - - private EasyEditPopupWindow mPopupWindow; - - private EasyEditSpan mEasyEditSpan; - - private Runnable mHidePopup; - - private void hide() { - if (mPopupWindow != null) { - mPopupWindow.hide(); - TextView.this.removeCallbacks(mHidePopup); - } - removeSpans(mText); - mEasyEditSpan = null; - } - - /** - * Monitors the changes in the text. - * - * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used, - * as the notifications are not sent when a spannable (with spans) is inserted. - */ - public void onTextChange(CharSequence buffer) { - adjustSpans(mText); - - if (getWindowVisibility() != View.VISIBLE) { - // The window is not visible yet, ignore the text change. - return; - } - - if (mLayout == null) { - // The view has not been layout yet, ignore the text change - return; - } - - InputMethodManager imm = InputMethodManager.peekInstance(); - if (!(TextView.this instanceof ExtractEditText) - && imm != null && imm.isFullscreenMode()) { - // The input is in extract mode. We do not have to handle the easy edit in the - // original TextView, as the ExtractEditText will do - return; - } - - // Remove the current easy edit span, as the text changed, and remove the pop-up - // (if any) - if (mEasyEditSpan != null) { - if (mText instanceof Spannable) { - ((Spannable) mText).removeSpan(mEasyEditSpan); - } - mEasyEditSpan = null; - } - if (mPopupWindow != null && mPopupWindow.isShowing()) { - mPopupWindow.hide(); - } - - // Display the new easy edit span (if any). - if (buffer instanceof Spanned) { - mEasyEditSpan = getSpan((Spanned) buffer); - if (mEasyEditSpan != null) { - if (mPopupWindow == null) { - mPopupWindow = new EasyEditPopupWindow(); - mHidePopup = new Runnable() { - @Override - public void run() { - hide(); - } - }; - } - mPopupWindow.show(mEasyEditSpan); - TextView.this.removeCallbacks(mHidePopup); - TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); - } - } - } - - /** - * Adjusts the spans by removing all of them except the last one. - */ - private void adjustSpans(CharSequence buffer) { - // This method enforces that only one easy edit span is attached to the text. - // A better way to enforce this would be to listen for onSpanAdded, but this method - // cannot be used in this scenario as no notification is triggered when a text with - // spans is inserted into a text. - if (buffer instanceof Spannable) { - Spannable spannable = (Spannable) buffer; - EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(), - EasyEditSpan.class); - for (int i = 0; i < spans.length - 1; i++) { - spannable.removeSpan(spans[i]); - } - } - } - - /** - * Removes all the {@link EasyEditSpan} currently attached. - */ - private void removeSpans(CharSequence buffer) { - if (buffer instanceof Spannable) { - Spannable spannable = (Spannable) buffer; - EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(), - EasyEditSpan.class); - for (int i = 0; i < spans.length; i++) { - spannable.removeSpan(spans[i]); - } - } - } - - private EasyEditSpan getSpan(Spanned spanned) { - EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(), - EasyEditSpan.class); - if (easyEditSpans.length == 0) { - return null; - } else { - return easyEditSpans[0]; - } - } - } - - /** - * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled - * by {@link EasyEditSpanController}. - */ - private class EasyEditPopupWindow extends PinnedPopupWindow - implements OnClickListener { - private static final int POPUP_TEXT_LAYOUT = - com.android.internal.R.layout.text_edit_action_popup_text; - private TextView mDeleteTextView; - private EasyEditSpan mEasyEditSpan; - - @Override - protected void createPopupWindow() { - mPopupWindow = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - mPopupWindow.setClippingEnabled(true); - } - - @Override - protected void initContentView() { - LinearLayout linearLayout = new LinearLayout(TextView.this.getContext()); - linearLayout.setOrientation(LinearLayout.HORIZONTAL); - mContentView = linearLayout; - mContentView.setBackgroundResource( - com.android.internal.R.drawable.text_edit_side_paste_window); - - LayoutInflater inflater = (LayoutInflater)TextView.this.mContext. - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - LayoutParams wrapContent = new LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mDeleteTextView.setLayoutParams(wrapContent); - mDeleteTextView.setText(com.android.internal.R.string.delete); - mDeleteTextView.setOnClickListener(this); - mContentView.addView(mDeleteTextView); - } - - public void show(EasyEditSpan easyEditSpan) { - mEasyEditSpan = easyEditSpan; - super.show(); - } - - @Override - public void onClick(View view) { - if (view == mDeleteTextView) { - Editable editable = (Editable) mText; - int start = editable.getSpanStart(mEasyEditSpan); - int end = editable.getSpanEnd(mEasyEditSpan); - if (start >= 0 && end >= 0) { - deleteText_internal(start, end); - } - } - } - - @Override - protected int getTextOffset() { - // Place the pop-up at the end of the span - Editable editable = (Editable) mText; - return editable.getSpanEnd(mEasyEditSpan); - } - - @Override - protected int getVerticalLocalPosition(int line) { - return mLayout.getLineBottom(line); - } - - @Override - protected int clipVertically(int positionY) { - // As we display the pop-up below the span, no vertical clipping is required. - return positionY; - } - } - private class ChangeWatcher implements TextWatcher, SpanWatcher { private CharSequence mBeforeText; - private EasyEditSpanController mEasyEditSpanController; - - private ChangeWatcher() { - mEasyEditSpanController = new EasyEditSpanController(); - } - public void beforeTextChanged(CharSequence buffer, int start, int before, int after) { if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start @@ -9547,14 +8247,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener TextView.this.sendBeforeTextChanged(buffer, start, before, after); } - public void onTextChanged(CharSequence buffer, int start, - int before, int after) { + public void onTextChanged(CharSequence buffer, int start, int before, int after) { if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); TextView.this.handleTextChanged(buffer, start, before, after); - mEasyEditSpanController.onTextChange(buffer); - if (AccessibilityManager.getInstance(mContext).isEnabled() && (isFocused() || isSelected() && isShown())) { sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after); @@ -9571,8 +8268,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - public void onSpanChanged(Spannable buf, - Object what, int s, int e, int st, int en) { + public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) { if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e + " st=" + st + " en=" + en + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, st, e, en); @@ -9589,2264 +8285,5 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, -1, e, -1); } - - private void hideControllers() { - mEasyEditSpanController.hide(); - } - } - - private static class Blink extends Handler implements Runnable { - private final WeakReference<TextView> mView; - private boolean mCancelled; - - public Blink(TextView v) { - mView = new WeakReference<TextView>(v); - } - - public void run() { - if (mCancelled) { - return; - } - - removeCallbacks(Blink.this); - - TextView tv = mView.get(); - - if (tv != null && tv.shouldBlink()) { - if (tv.mLayout != null) { - tv.invalidateCursorPath(); - } - - postAtTime(this, SystemClock.uptimeMillis() + BLINK); - } - } - - void cancel() { - if (!mCancelled) { - removeCallbacks(Blink.this); - mCancelled = true; - } - } - - void uncancel() { - mCancelled = false; - } - } - - private static class DragLocalState { - public TextView sourceTextView; - public int start, end; - - public DragLocalState(TextView sourceTextView, int start, int end) { - this.sourceTextView = sourceTextView; - this.start = start; - this.end = end; - } - } - - private class PositionListener implements ViewTreeObserver.OnPreDrawListener { - // 3 handles - // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) - private final int MAXIMUM_NUMBER_OF_LISTENERS = 6; - private TextViewPositionListener[] mPositionListeners = - new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; - private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; - private boolean mPositionHasChanged = true; - // Absolute position of the TextView with respect to its parent window - private int mPositionX, mPositionY; - private int mNumberOfListeners; - private boolean mScrollHasChanged; - final int[] mTempCoords = new int[2]; - - public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) { - if (mNumberOfListeners == 0) { - updatePosition(); - ViewTreeObserver vto = TextView.this.getViewTreeObserver(); - vto.addOnPreDrawListener(this); - } - - int emptySlotIndex = -1; - for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { - TextViewPositionListener listener = mPositionListeners[i]; - if (listener == positionListener) { - return; - } else if (emptySlotIndex < 0 && listener == null) { - emptySlotIndex = i; - } - } - - mPositionListeners[emptySlotIndex] = positionListener; - mCanMove[emptySlotIndex] = canMove; - mNumberOfListeners++; - } - - public void removeSubscriber(TextViewPositionListener positionListener) { - for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { - if (mPositionListeners[i] == positionListener) { - mPositionListeners[i] = null; - mNumberOfListeners--; - break; - } - } - - if (mNumberOfListeners == 0) { - ViewTreeObserver vto = TextView.this.getViewTreeObserver(); - vto.removeOnPreDrawListener(this); - } - } - - public int getPositionX() { - return mPositionX; - } - - public int getPositionY() { - return mPositionY; - } - - @Override - public boolean onPreDraw() { - updatePosition(); - - for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { - if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) { - TextViewPositionListener positionListener = mPositionListeners[i]; - if (positionListener != null) { - positionListener.updatePosition(mPositionX, mPositionY, - mPositionHasChanged, mScrollHasChanged); - } - } - } - - mScrollHasChanged = false; - return true; - } - - private void updatePosition() { - TextView.this.getLocationInWindow(mTempCoords); - - mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY; - - mPositionX = mTempCoords[0]; - mPositionY = mTempCoords[1]; - } - - public void onScrollChanged() { - mScrollHasChanged = true; - } - } - - private abstract class PinnedPopupWindow implements TextViewPositionListener { - protected PopupWindow mPopupWindow; - protected ViewGroup mContentView; - int mPositionX, mPositionY; - - protected abstract void createPopupWindow(); - protected abstract void initContentView(); - protected abstract int getTextOffset(); - protected abstract int getVerticalLocalPosition(int line); - protected abstract int clipVertically(int positionY); - - public PinnedPopupWindow() { - createPopupWindow(); - - mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); - mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); - mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); - - initContentView(); - - LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - mContentView.setLayoutParams(wrapContent); - - mPopupWindow.setContentView(mContentView); - } - - public void show() { - TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */); - - computeLocalPosition(); - - final PositionListener positionListener = TextView.this.getPositionListener(); - updatePosition(positionListener.getPositionX(), positionListener.getPositionY()); - } - - protected void measureContent() { - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - mContentView.measure( - View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, - View.MeasureSpec.AT_MOST), - View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, - View.MeasureSpec.AT_MOST)); - } - - /* The popup window will be horizontally centered on the getTextOffset() and vertically - * positioned according to viewportToContentHorizontalOffset. - * - * This method assumes that mContentView has properly been measured from its content. */ - private void computeLocalPosition() { - measureContent(); - final int width = mContentView.getMeasuredWidth(); - final int offset = getTextOffset(); - mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f); - mPositionX += viewportToContentHorizontalOffset(); - - final int line = mLayout.getLineForOffset(offset); - mPositionY = getVerticalLocalPosition(line); - mPositionY += viewportToContentVerticalOffset(); - } - - private void updatePosition(int parentPositionX, int parentPositionY) { - int positionX = parentPositionX + mPositionX; - int positionY = parentPositionY + mPositionY; - - positionY = clipVertically(positionY); - - // Horizontal clipping - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - final int width = mContentView.getMeasuredWidth(); - positionX = Math.min(displayMetrics.widthPixels - width, positionX); - positionX = Math.max(0, positionX); - - if (isShowing()) { - mPopupWindow.update(positionX, positionY, -1, -1); - } else { - mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY, - positionX, positionY); - } - } - - public void hide() { - mPopupWindow.dismiss(); - TextView.this.getPositionListener().removeSubscriber(this); - } - - @Override - public void updatePosition(int parentPositionX, int parentPositionY, - boolean parentPositionChanged, boolean parentScrolled) { - // Either parentPositionChanged or parentScrolled is true, check if still visible - if (isShowing() && isOffsetVisible(getTextOffset())) { - if (parentScrolled) computeLocalPosition(); - updatePosition(parentPositionX, parentPositionY); - } else { - hide(); - } - } - - public boolean isShowing() { - return mPopupWindow.isShowing(); - } - } - - private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener { - private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; - private static final int ADD_TO_DICTIONARY = -1; - private static final int DELETE_TEXT = -2; - private SuggestionInfo[] mSuggestionInfos; - private int mNumberOfSuggestions; - private boolean mCursorWasVisibleBeforeSuggestions; - private boolean mIsShowingUp = false; - private SuggestionAdapter mSuggestionsAdapter; - private final Comparator<SuggestionSpan> mSuggestionSpanComparator; - private final HashMap<SuggestionSpan, Integer> mSpansLengths; - - private class CustomPopupWindow extends PopupWindow { - public CustomPopupWindow(Context context, int defStyle) { - super(context, null, defStyle); - } - - @Override - public void dismiss() { - super.dismiss(); - - TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this); - - // Safe cast since show() checks that mText is an Editable - ((Spannable) mText).removeSpan(getEditor().mSuggestionRangeSpan); - - setCursorVisible(mCursorWasVisibleBeforeSuggestions); - if (hasInsertionController()) { - getInsertionController().show(); - } - } - } - - public SuggestionsPopupWindow() { - mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible; - mSuggestionSpanComparator = new SuggestionSpanComparator(); - mSpansLengths = new HashMap<SuggestionSpan, Integer>(); - } - - @Override - protected void createPopupWindow() { - mPopupWindow = new CustomPopupWindow(TextView.this.mContext, - com.android.internal.R.attr.textSuggestionsWindowStyle); - mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - mPopupWindow.setFocusable(true); - mPopupWindow.setClippingEnabled(false); - } - - @Override - protected void initContentView() { - ListView listView = new ListView(TextView.this.getContext()); - mSuggestionsAdapter = new SuggestionAdapter(); - listView.setAdapter(mSuggestionsAdapter); - listView.setOnItemClickListener(this); - mContentView = listView; - - // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete - mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2]; - for (int i = 0; i < mSuggestionInfos.length; i++) { - mSuggestionInfos[i] = new SuggestionInfo(); - } - } - - public boolean isShowingUp() { - return mIsShowingUp; - } - - public void onParentLostFocus() { - mIsShowingUp = false; - } - - private class SuggestionInfo { - int suggestionStart, suggestionEnd; // range of actual suggestion within text - SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents - int suggestionIndex; // the index of this suggestion inside suggestionSpan - SpannableStringBuilder text = new SpannableStringBuilder(); - TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext, - android.R.style.TextAppearance_SuggestionHighlight); - } - - private class SuggestionAdapter extends BaseAdapter { - private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext. - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - @Override - public int getCount() { - return mNumberOfSuggestions; - } - - @Override - public Object getItem(int position) { - return mSuggestionInfos[position]; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - TextView textView = (TextView) convertView; - - if (textView == null) { - textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent, - false); - } - - final SuggestionInfo suggestionInfo = mSuggestionInfos[position]; - textView.setText(suggestionInfo.text); - - if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { - textView.setCompoundDrawablesWithIntrinsicBounds( - com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0); - } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) { - textView.setCompoundDrawablesWithIntrinsicBounds( - com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0); - } else { - textView.setCompoundDrawables(null, null, null, null); - } - - return textView; - } - } - - private class SuggestionSpanComparator implements Comparator<SuggestionSpan> { - public int compare(SuggestionSpan span1, SuggestionSpan span2) { - final int flag1 = span1.getFlags(); - final int flag2 = span2.getFlags(); - if (flag1 != flag2) { - // The order here should match what is used in updateDrawState - final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; - final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; - final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0; - final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0; - if (easy1 && !misspelled1) return -1; - if (easy2 && !misspelled2) return 1; - if (misspelled1) return -1; - if (misspelled2) return 1; - } - - return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue(); - } - } - - /** - * Returns the suggestion spans that cover the current cursor position. The suggestion - * spans are sorted according to the length of text that they are attached to. - */ - private SuggestionSpan[] getSuggestionSpans() { - int pos = TextView.this.getSelectionStart(); - Spannable spannable = (Spannable) TextView.this.mText; - SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); - - mSpansLengths.clear(); - for (SuggestionSpan suggestionSpan : suggestionSpans) { - int start = spannable.getSpanStart(suggestionSpan); - int end = spannable.getSpanEnd(suggestionSpan); - mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start)); - } - - // The suggestions are sorted according to their types (easy correction first, then - // misspelled) and to the length of the text that they cover (shorter first). - Arrays.sort(suggestionSpans, mSuggestionSpanComparator); - return suggestionSpans; - } - - @Override - public void show() { - if (!(mText instanceof Editable)) return; - - if (updateSuggestions()) { - mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible; - setCursorVisible(false); - mIsShowingUp = true; - super.show(); - } - } - - @Override - protected void measureContent() { - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec( - displayMetrics.widthPixels, View.MeasureSpec.AT_MOST); - final int verticalMeasure = View.MeasureSpec.makeMeasureSpec( - displayMetrics.heightPixels, View.MeasureSpec.AT_MOST); - - int width = 0; - View view = null; - for (int i = 0; i < mNumberOfSuggestions; i++) { - view = mSuggestionsAdapter.getView(i, view, mContentView); - view.getLayoutParams().width = LayoutParams.WRAP_CONTENT; - view.measure(horizontalMeasure, verticalMeasure); - width = Math.max(width, view.getMeasuredWidth()); - } - - // Enforce the width based on actual text widths - mContentView.measure( - View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), - verticalMeasure); - - Drawable popupBackground = mPopupWindow.getBackground(); - if (popupBackground != null) { - if (mTempRect == null) mTempRect = new Rect(); - popupBackground.getPadding(mTempRect); - width += mTempRect.left + mTempRect.right; - } - mPopupWindow.setWidth(width); - } - - @Override - protected int getTextOffset() { - return getSelectionStart(); - } - - @Override - protected int getVerticalLocalPosition(int line) { - return mLayout.getLineBottom(line); - } - - @Override - protected int clipVertically(int positionY) { - final int height = mContentView.getMeasuredHeight(); - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - return Math.min(positionY, displayMetrics.heightPixels - height); - } - - @Override - public void hide() { - super.hide(); - } - - private boolean updateSuggestions() { - Spannable spannable = (Spannable) TextView.this.mText; - SuggestionSpan[] suggestionSpans = getSuggestionSpans(); - - final int nbSpans = suggestionSpans.length; - // Suggestions are shown after a delay: the underlying spans may have been removed - if (nbSpans == 0) return false; - - mNumberOfSuggestions = 0; - int spanUnionStart = mText.length(); - int spanUnionEnd = 0; - - SuggestionSpan misspelledSpan = null; - int underlineColor = 0; - - for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { - SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; - final int spanStart = spannable.getSpanStart(suggestionSpan); - final int spanEnd = spannable.getSpanEnd(suggestionSpan); - spanUnionStart = Math.min(spanStart, spanUnionStart); - spanUnionEnd = Math.max(spanEnd, spanUnionEnd); - - if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) { - misspelledSpan = suggestionSpan; - } - - // The first span dictates the background color of the highlighted text - if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor(); - - String[] suggestions = suggestionSpan.getSuggestions(); - int nbSuggestions = suggestions.length; - for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { - String suggestion = suggestions[suggestionIndex]; - - boolean suggestionIsDuplicate = false; - for (int i = 0; i < mNumberOfSuggestions; i++) { - if (mSuggestionInfos[i].text.toString().equals(suggestion)) { - SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan; - final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan); - final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan); - if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) { - suggestionIsDuplicate = true; - break; - } - } - } - - if (!suggestionIsDuplicate) { - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = suggestionSpan; - suggestionInfo.suggestionIndex = suggestionIndex; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion); - - mNumberOfSuggestions++; - - if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) { - // Also end outer for loop - spanIndex = nbSpans; - break; - } - } - } - } - - for (int i = 0; i < mNumberOfSuggestions; i++) { - highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); - } - - // Add "Add to dictionary" item if there is a span with the misspelled flag - if (misspelledSpan != null) { - final int misspelledStart = spannable.getSpanStart(misspelledSpan); - final int misspelledEnd = spannable.getSpanEnd(misspelledSpan); - if (misspelledStart >= 0 && misspelledEnd > misspelledStart) { - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = misspelledSpan; - suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), - getContext().getString(com.android.internal.R.string.addToDictionary)); - suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - mNumberOfSuggestions++; - } - } - - // Delete item - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = null; - suggestionInfo.suggestionIndex = DELETE_TEXT; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), - getContext().getString(com.android.internal.R.string.deleteText)); - suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - mNumberOfSuggestions++; - - if (getEditor().mSuggestionRangeSpan == null) getEditor().mSuggestionRangeSpan = new SuggestionRangeSpan(); - if (underlineColor == 0) { - // Fallback on the default highlight color when the first span does not provide one - getEditor().mSuggestionRangeSpan.setBackgroundColor(mHighlightColor); - } else { - final float BACKGROUND_TRANSPARENCY = 0.4f; - final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY); - getEditor().mSuggestionRangeSpan.setBackgroundColor( - (underlineColor & 0x00FFFFFF) + (newAlpha << 24)); - } - spannable.setSpan(getEditor().mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - mSuggestionsAdapter.notifyDataSetChanged(); - return true; - } - - private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, - int unionEnd) { - final Spannable text = (Spannable) mText; - final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan); - final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan); - - // Adjust the start/end of the suggestion span - suggestionInfo.suggestionStart = spanStart - unionStart; - suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart - + suggestionInfo.text.length(); - - suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, - suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - // Add the text before and after the span. - final String textAsString = text.toString(); - suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart)); - suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd)); - } - - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - Editable editable = (Editable) mText; - SuggestionInfo suggestionInfo = mSuggestionInfos[position]; - - if (suggestionInfo.suggestionIndex == DELETE_TEXT) { - final int spanUnionStart = editable.getSpanStart(getEditor().mSuggestionRangeSpan); - int spanUnionEnd = editable.getSpanEnd(getEditor().mSuggestionRangeSpan); - if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { - // Do not leave two adjacent spaces after deletion, or one at beginning of text - if (spanUnionEnd < editable.length() && - Character.isSpaceChar(editable.charAt(spanUnionEnd)) && - (spanUnionStart == 0 || - Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) { - spanUnionEnd = spanUnionEnd + 1; - } - deleteText_internal(spanUnionStart, spanUnionEnd); - } - hide(); - return; - } - - final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan); - final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan); - if (spanStart < 0 || spanEnd <= spanStart) { - // Span has been removed - hide(); - return; - } - final String originalText = mText.toString().substring(spanStart, spanEnd); - - if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { - Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); - intent.putExtra("word", originalText); - intent.putExtra("locale", getTextServicesLocale().toString()); - intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); - getContext().startActivity(intent); - // There is no way to know if the word was indeed added. Re-check. - // TODO The ExtractEditText should remove the span in the original text instead - editable.removeSpan(suggestionInfo.suggestionSpan); - updateSpellCheckSpans(spanStart, spanEnd, false); - } else { - // SuggestionSpans are removed by replace: save them before - SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, - SuggestionSpan.class); - final int length = suggestionSpans.length; - int[] suggestionSpansStarts = new int[length]; - int[] suggestionSpansEnds = new int[length]; - int[] suggestionSpansFlags = new int[length]; - for (int i = 0; i < length; i++) { - final SuggestionSpan suggestionSpan = suggestionSpans[i]; - suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); - suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); - suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); - - // Remove potential misspelled flags - int suggestionSpanFlags = suggestionSpan.getFlags(); - if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) { - suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; - suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; - suggestionSpan.setFlags(suggestionSpanFlags); - } - } - - final int suggestionStart = suggestionInfo.suggestionStart; - final int suggestionEnd = suggestionInfo.suggestionEnd; - final String suggestion = suggestionInfo.text.subSequence( - suggestionStart, suggestionEnd).toString(); - replaceText_internal(spanStart, spanEnd, suggestion); - - // Notify source IME of the suggestion pick. Do this before swaping texts. - if (!TextUtils.isEmpty( - suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText, - suggestionInfo.suggestionIndex); - } - } - - // Swap text content between actual text and Suggestion span - String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); - suggestions[suggestionInfo.suggestionIndex] = originalText; - - // Restore previous SuggestionSpans - final int lengthDifference = suggestion.length() - (spanEnd - spanStart); - for (int i = 0; i < length; i++) { - // Only spans that include the modified region make sense after replacement - // Spans partially included in the replaced region are removed, there is no - // way to assign them a valid range after replacement - if (suggestionSpansStarts[i] <= spanStart && - suggestionSpansEnds[i] >= spanEnd) { - setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], - suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]); - } - } - - // Move cursor at the end of the replaced word - final int newCursorPosition = spanEnd + lengthDifference; - setCursorPosition_internal(newCursorPosition, newCursorPosition); - } - - hide(); - } - } - - /** - * An ActionMode Callback class that is used to provide actions while in text selection mode. - * - * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending - * on which of these this TextView supports. - */ - private class SelectionActionModeCallback implements ActionMode.Callback { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - TypedArray styledAttributes = mContext.obtainStyledAttributes( - com.android.internal.R.styleable.SelectionModeDrawables); - - boolean allowText = getContext().getResources().getBoolean( - com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon); - - mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle)); - mode.setSubtitle(null); - mode.setTitleOptionalHint(true); - - int selectAllIconId = 0; // No icon by default - if (!allowText) { - // Provide an icon, text will not be displayed on smaller screens. - selectAllIconId = styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0); - } - - menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). - setIcon(selectAllIconId). - setAlphabeticShortcut('a'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - - if (canCut()) { - menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut). - setIcon(styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)). - setAlphabeticShortcut('x'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - - if (canCopy()) { - menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). - setIcon(styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)). - setAlphabeticShortcut('c'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - - if (canPaste()) { - menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). - setIcon(styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)). - setAlphabeticShortcut('v'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - - styledAttributes.recycle(); - - if (getEditor().mCustomSelectionActionModeCallback != null) { - if (!getEditor().mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) { - // The custom mode can choose to cancel the action mode - return false; - } - } - - if (menu.hasVisibleItems() || mode.getCustomView() != null) { - getSelectionController().show(); - return true; - } else { - return false; - } - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - if (getEditor().mCustomSelectionActionModeCallback != null) { - return getEditor().mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu); - } - return true; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (getEditor().mCustomSelectionActionModeCallback != null && - getEditor().mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) { - return true; - } - return onTextContextMenuItem(item.getItemId()); - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - if (getEditor().mCustomSelectionActionModeCallback != null) { - getEditor().mCustomSelectionActionModeCallback.onDestroyActionMode(mode); - } - Selection.setSelection((Spannable) mText, getSelectionEnd()); - - if (getEditor().mSelectionModifierCursorController != null) { - getEditor().mSelectionModifierCursorController.hide(); - } - - getEditor().mSelectionActionMode = null; - } - } - - private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener { - private static final int POPUP_TEXT_LAYOUT = - com.android.internal.R.layout.text_edit_action_popup_text; - private TextView mPasteTextView; - private TextView mReplaceTextView; - - @Override - protected void createPopupWindow() { - mPopupWindow = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mPopupWindow.setClippingEnabled(true); - } - - @Override - protected void initContentView() { - LinearLayout linearLayout = new LinearLayout(TextView.this.getContext()); - linearLayout.setOrientation(LinearLayout.HORIZONTAL); - mContentView = linearLayout; - mContentView.setBackgroundResource( - com.android.internal.R.drawable.text_edit_paste_window); - - LayoutInflater inflater = (LayoutInflater)TextView.this.mContext. - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - LayoutParams wrapContent = new LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mPasteTextView.setLayoutParams(wrapContent); - mContentView.addView(mPasteTextView); - mPasteTextView.setText(com.android.internal.R.string.paste); - mPasteTextView.setOnClickListener(this); - - mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mReplaceTextView.setLayoutParams(wrapContent); - mContentView.addView(mReplaceTextView); - mReplaceTextView.setText(com.android.internal.R.string.replace); - mReplaceTextView.setOnClickListener(this); - } - - @Override - public void show() { - boolean canPaste = canPaste(); - boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan(); - mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE); - mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE); - - if (!canPaste && !canSuggest) return; - - super.show(); - } - - @Override - public void onClick(View view) { - if (view == mPasteTextView && canPaste()) { - onTextContextMenuItem(ID_PASTE); - hide(); - } else if (view == mReplaceTextView) { - final int middle = (getSelectionStart() + getSelectionEnd()) / 2; - stopSelectionActionMode(); - Selection.setSelection((Spannable) mText, middle); - showSuggestions(); - } - } - - @Override - protected int getTextOffset() { - return (getSelectionStart() + getSelectionEnd()) / 2; - } - - @Override - protected int getVerticalLocalPosition(int line) { - return mLayout.getLineTop(line) - mContentView.getMeasuredHeight(); - } - - @Override - protected int clipVertically(int positionY) { - if (positionY < 0) { - final int offset = getTextOffset(); - final int line = mLayout.getLineForOffset(offset); - positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line); - positionY += mContentView.getMeasuredHeight(); - - // Assumes insertion and selection handles share the same height - final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes); - positionY += handle.getIntrinsicHeight(); - } - - return positionY; - } - } - - private abstract class HandleView extends View implements TextViewPositionListener { - protected Drawable mDrawable; - protected Drawable mDrawableLtr; - protected Drawable mDrawableRtl; - private final PopupWindow mContainer; - // Position with respect to the parent TextView - private int mPositionX, mPositionY; - private boolean mIsDragging; - // Offset from touch position to mPosition - private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; - protected int mHotspotX; - // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up - private float mTouchOffsetY; - // Where the touch position should be on the handle to ensure a maximum cursor visibility - private float mIdealVerticalOffset; - // Parent's (TextView) previous position in window - private int mLastParentX, mLastParentY; - // Transient action popup window for Paste and Replace actions - protected ActionPopupWindow mActionPopupWindow; - // Previous text character offset - private int mPreviousOffset = -1; - // Previous text character offset - private boolean mPositionHasChanged = true; - // Used to delay the appearance of the action popup window - private Runnable mActionPopupShower; - - public HandleView(Drawable drawableLtr, Drawable drawableRtl) { - super(TextView.this.mContext); - mContainer = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mContainer.setSplitTouchEnabled(true); - mContainer.setClippingEnabled(false); - mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); - mContainer.setContentView(this); - - mDrawableLtr = drawableLtr; - mDrawableRtl = drawableRtl; - - updateDrawable(); - - final int handleHeight = mDrawable.getIntrinsicHeight(); - mTouchOffsetY = -0.3f * handleHeight; - mIdealVerticalOffset = 0.7f * handleHeight; - } - - protected void updateDrawable() { - final int offset = getCurrentCursorOffset(); - final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset); - mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr; - mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset); - } - - protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun); - - // Touch-up filter: number of previous positions remembered - private static final int HISTORY_SIZE = 5; - private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150; - private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350; - private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE]; - private final int[] mPreviousOffsets = new int[HISTORY_SIZE]; - private int mPreviousOffsetIndex = 0; - private int mNumberPreviousOffsets = 0; - - private void startTouchUpFilter(int offset) { - mNumberPreviousOffsets = 0; - addPositionToTouchUpFilter(offset); - } - - private void addPositionToTouchUpFilter(int offset) { - mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE; - mPreviousOffsets[mPreviousOffsetIndex] = offset; - mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis(); - mNumberPreviousOffsets++; - } - - private void filterOnTouchUp() { - final long now = SystemClock.uptimeMillis(); - int i = 0; - int index = mPreviousOffsetIndex; - final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE); - while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) { - i++; - index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE; - } - - if (i > 0 && i < iMax && - (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { - positionAtCursorOffset(mPreviousOffsets[index], false); - } - } - - public boolean offsetHasBeenChanged() { - return mNumberPreviousOffsets > 1; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); - } - - public void show() { - if (isShowing()) return; - - getPositionListener().addSubscriber(this, true /* local position may change */); - - // Make sure the offset is always considered new, even when focusing at same position - mPreviousOffset = -1; - positionAtCursorOffset(getCurrentCursorOffset(), false); - - hideActionPopupWindow(); - } - - protected void dismiss() { - mIsDragging = false; - mContainer.dismiss(); - onDetached(); - } - - public void hide() { - dismiss(); - - TextView.this.getPositionListener().removeSubscriber(this); - } - - void showActionPopupWindow(int delay) { - if (mActionPopupWindow == null) { - mActionPopupWindow = new ActionPopupWindow(); - } - if (mActionPopupShower == null) { - mActionPopupShower = new Runnable() { - public void run() { - mActionPopupWindow.show(); - } - }; - } else { - TextView.this.removeCallbacks(mActionPopupShower); - } - TextView.this.postDelayed(mActionPopupShower, delay); - } - - protected void hideActionPopupWindow() { - if (mActionPopupShower != null) { - TextView.this.removeCallbacks(mActionPopupShower); - } - if (mActionPopupWindow != null) { - mActionPopupWindow.hide(); - } - } - - public boolean isShowing() { - return mContainer.isShowing(); - } - - private boolean isVisible() { - // Always show a dragging handle. - if (mIsDragging) { - return true; - } - - if (isInBatchEditMode()) { - return false; - } - - return TextView.this.isPositionVisible(mPositionX + mHotspotX, mPositionY); - } - - public abstract int getCurrentCursorOffset(); - - protected abstract void updateSelection(int offset); - - public abstract void updatePosition(float x, float y); - - protected void positionAtCursorOffset(int offset, boolean parentScrolled) { - // A HandleView relies on the layout, which may be nulled by external methods - if (mLayout == null) { - // Will update controllers' state, hiding them and stopping selection mode if needed - prepareCursorControllers(); - return; - } - - boolean offsetChanged = offset != mPreviousOffset; - if (offsetChanged || parentScrolled) { - if (offsetChanged) { - updateSelection(offset); - addPositionToTouchUpFilter(offset); - } - final int line = mLayout.getLineForOffset(offset); - - mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX); - mPositionY = mLayout.getLineBottom(line); - - // Take TextView's padding and scroll into account. - mPositionX += viewportToContentHorizontalOffset(); - mPositionY += viewportToContentVerticalOffset(); - - mPreviousOffset = offset; - mPositionHasChanged = true; - } - } - - public void updatePosition(int parentPositionX, int parentPositionY, - boolean parentPositionChanged, boolean parentScrolled) { - positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled); - if (parentPositionChanged || mPositionHasChanged) { - if (mIsDragging) { - // Update touchToWindow offset in case of parent scrolling while dragging - if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) { - mTouchToWindowOffsetX += parentPositionX - mLastParentX; - mTouchToWindowOffsetY += parentPositionY - mLastParentY; - mLastParentX = parentPositionX; - mLastParentY = parentPositionY; - } - - onHandleMoved(); - } - - if (isVisible()) { - final int positionX = parentPositionX + mPositionX; - final int positionY = parentPositionY + mPositionY; - if (isShowing()) { - mContainer.update(positionX, positionY, -1, -1); - } else { - mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, - positionX, positionY); - } - } else { - if (isShowing()) { - dismiss(); - } - } - - mPositionHasChanged = false; - } - } - - @Override - protected void onDraw(Canvas c) { - mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop); - mDrawable.draw(c); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - startTouchUpFilter(getCurrentCursorOffset()); - mTouchToWindowOffsetX = ev.getRawX() - mPositionX; - mTouchToWindowOffsetY = ev.getRawY() - mPositionY; - - final PositionListener positionListener = getPositionListener(); - mLastParentX = positionListener.getPositionX(); - mLastParentY = positionListener.getPositionY(); - mIsDragging = true; - break; - } - - case MotionEvent.ACTION_MOVE: { - final float rawX = ev.getRawX(); - final float rawY = ev.getRawY(); - - // Vertical hysteresis: vertical down movement tends to snap to ideal offset - final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY; - final float currentVerticalOffset = rawY - mPositionY - mLastParentY; - float newVerticalOffset; - if (previousVerticalOffset < mIdealVerticalOffset) { - newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset); - newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset); - } else { - newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset); - newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset); - } - mTouchToWindowOffsetY = newVerticalOffset + mLastParentY; - - final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; - final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY; - - updatePosition(newPosX, newPosY); - break; - } - - case MotionEvent.ACTION_UP: - filterOnTouchUp(); - mIsDragging = false; - break; - - case MotionEvent.ACTION_CANCEL: - mIsDragging = false; - break; - } - return true; - } - - public boolean isDragging() { - return mIsDragging; - } - - void onHandleMoved() { - hideActionPopupWindow(); - } - - public void onDetached() { - hideActionPopupWindow(); - } - } - - private class InsertionHandleView extends HandleView { - private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; - private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds - - // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow - private float mDownPositionX, mDownPositionY; - private Runnable mHider; - - public InsertionHandleView(Drawable drawable) { - super(drawable, drawable); - } - - @Override - public void show() { - super.show(); - - final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - LAST_CUT_OR_COPY_TIME; - if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { - showActionPopupWindow(0); - } - - hideAfterDelay(); - } - - public void showWithActionPopup() { - show(); - showActionPopupWindow(0); - } - - private void hideAfterDelay() { - if (mHider == null) { - mHider = new Runnable() { - public void run() { - hide(); - } - }; - } else { - removeHiderCallback(); - } - TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT); - } - - private void removeHiderCallback() { - if (mHider != null) { - TextView.this.removeCallbacks(mHider); - } - } - - @Override - protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - return drawable.getIntrinsicWidth() / 2; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - final boolean result = super.onTouchEvent(ev); - - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mDownPositionX = ev.getRawX(); - mDownPositionY = ev.getRawY(); - break; - - case MotionEvent.ACTION_UP: - if (!offsetHasBeenChanged()) { - final float deltaX = mDownPositionX - ev.getRawX(); - final float deltaY = mDownPositionY - ev.getRawY(); - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - final ViewConfiguration viewConfiguration = ViewConfiguration.get( - TextView.this.getContext()); - final int touchSlop = viewConfiguration.getScaledTouchSlop(); - - if (distanceSquared < touchSlop * touchSlop) { - if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) { - // Tapping on the handle dismisses the displayed action popup - mActionPopupWindow.hide(); - } else { - showWithActionPopup(); - } - } - } - hideAfterDelay(); - break; - - case MotionEvent.ACTION_CANCEL: - hideAfterDelay(); - break; - - default: - break; - } - - return result; - } - - @Override - public int getCurrentCursorOffset() { - return TextView.this.getSelectionStart(); - } - - @Override - public void updateSelection(int offset) { - Selection.setSelection((Spannable) mText, offset); - } - - @Override - public void updatePosition(float x, float y) { - positionAtCursorOffset(getOffsetForPosition(x, y), false); - } - - @Override - void onHandleMoved() { - super.onHandleMoved(); - removeHiderCallback(); - } - - @Override - public void onDetached() { - super.onDetached(); - removeHiderCallback(); - } - } - - private class SelectionStartHandleView extends HandleView { - - public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) { - super(drawableLtr, drawableRtl); - } - - @Override - protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return drawable.getIntrinsicWidth() / 4; - } else { - return (drawable.getIntrinsicWidth() * 3) / 4; - } - } - - @Override - public int getCurrentCursorOffset() { - return TextView.this.getSelectionStart(); - } - - @Override - public void updateSelection(int offset) { - Selection.setSelection((Spannable) mText, offset, getSelectionEnd()); - updateDrawable(); - } - - @Override - public void updatePosition(float x, float y) { - int offset = getOffsetForPosition(x, y); - - // Handles can not cross and selection is at least one character - final int selectionEnd = getSelectionEnd(); - if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1); - - positionAtCursorOffset(offset, false); - } - - public ActionPopupWindow getActionPopupWindow() { - return mActionPopupWindow; - } - } - - private class SelectionEndHandleView extends HandleView { - - public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) { - super(drawableLtr, drawableRtl); - } - - @Override - protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return (drawable.getIntrinsicWidth() * 3) / 4; - } else { - return drawable.getIntrinsicWidth() / 4; - } - } - - @Override - public int getCurrentCursorOffset() { - return TextView.this.getSelectionEnd(); - } - - @Override - public void updateSelection(int offset) { - Selection.setSelection((Spannable) mText, getSelectionStart(), offset); - updateDrawable(); - } - - @Override - public void updatePosition(float x, float y) { - int offset = getOffsetForPosition(x, y); - - // Handles can not cross and selection is at least one character - final int selectionStart = getSelectionStart(); - if (offset <= selectionStart) offset = Math.min(selectionStart + 1, mText.length()); - - positionAtCursorOffset(offset, false); - } - - public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) { - mActionPopupWindow = actionPopupWindow; - } - } - - /** - * A CursorController instance can be used to control a cursor in the text. - * It is not used outside of {@link TextView}. - * @hide - */ - private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { - /** - * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. - * See also {@link #hide()}. - */ - public void show(); - - /** - * Hide the cursor controller from screen. - * See also {@link #show()}. - */ - public void hide(); - - /** - * Called when the view is detached from window. Perform house keeping task, such as - * stopping Runnable thread that would otherwise keep a reference on the context, thus - * preventing the activity from being recycled. - */ - public void onDetached(); - } - - private class InsertionPointCursorController implements CursorController { - private InsertionHandleView mHandle; - - public void show() { - getHandle().show(); - } - - public void showWithActionPopup() { - getHandle().showWithActionPopup(); - } - - public void hide() { - if (mHandle != null) { - mHandle.hide(); - } - } - - public void onTouchModeChanged(boolean isInTouchMode) { - if (!isInTouchMode) { - hide(); - } - } - - private InsertionHandleView getHandle() { - if (getEditor().mSelectHandleCenter == null) { - getEditor().mSelectHandleCenter = mContext.getResources().getDrawable( - mTextSelectHandleRes); - } - if (mHandle == null) { - mHandle = new InsertionHandleView(getEditor().mSelectHandleCenter); - } - return mHandle; - } - - @Override - public void onDetached() { - final ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnTouchModeChangeListener(this); - - if (mHandle != null) mHandle.onDetached(); - } - } - - private class SelectionModifierCursorController implements CursorController { - private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds - // The cursor controller handles, lazily created when shown. - private SelectionStartHandleView mStartHandle; - private SelectionEndHandleView mEndHandle; - // The offsets of that last touch down event. Remembered to start selection there. - private int mMinTouchOffset, mMaxTouchOffset; - - // Double tap detection - private long mPreviousTapUpTime = 0; - private float mDownPositionX, mDownPositionY; - private boolean mGestureStayedInTapRegion; - - SelectionModifierCursorController() { - resetTouchOffsets(); - } - - public void show() { - if (isInBatchEditMode()) { - return; - } - initDrawables(); - initHandles(); - hideInsertionPointCursorController(); - } - - private void initDrawables() { - if (getEditor().mSelectHandleLeft == null) { - getEditor().mSelectHandleLeft = mContext.getResources().getDrawable( - mTextSelectHandleLeftRes); - } - if (getEditor().mSelectHandleRight == null) { - getEditor().mSelectHandleRight = mContext.getResources().getDrawable( - mTextSelectHandleRightRes); - } - } - - private void initHandles() { - // Lazy object creation has to be done before updatePosition() is called. - if (mStartHandle == null) { - mStartHandle = new SelectionStartHandleView(getEditor().mSelectHandleLeft, getEditor().mSelectHandleRight); - } - if (mEndHandle == null) { - mEndHandle = new SelectionEndHandleView(getEditor().mSelectHandleRight, getEditor().mSelectHandleLeft); - } - - mStartHandle.show(); - mEndHandle.show(); - - // Make sure both left and right handles share the same ActionPopupWindow (so that - // moving any of the handles hides the action popup). - mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION); - mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow()); - - hideInsertionPointCursorController(); - } - - public void hide() { - if (mStartHandle != null) mStartHandle.hide(); - if (mEndHandle != null) mEndHandle.hide(); - } - - public void onTouchEvent(MotionEvent event) { - // This is done even when the View does not have focus, so that long presses can start - // selection and tap can move cursor from this tap position. - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - final float x = event.getX(); - final float y = event.getY(); - - // Remember finger down position, to be able to start selection from there - mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); - - // Double tap detection - if (mGestureStayedInTapRegion) { - long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; - if (duration <= ViewConfiguration.getDoubleTapTimeout()) { - final float deltaX = x - mDownPositionX; - final float deltaY = y - mDownPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - ViewConfiguration viewConfiguration = ViewConfiguration.get( - TextView.this.getContext()); - int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); - boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop; - - if (stayedInArea && isPositionOnText(x, y)) { - startSelectionActionMode(); - getEditor().mDiscardNextActionUp = true; - } - } - } - - mDownPositionX = x; - mDownPositionY = y; - mGestureStayedInTapRegion = true; - break; - - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: - // Handle multi-point gestures. Keep min and max offset positions. - // Only activated for devices that correctly handle multi-touch. - if (mContext.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { - updateMinAndMaxOffsets(event); - } - break; - - case MotionEvent.ACTION_MOVE: - if (mGestureStayedInTapRegion) { - final float deltaX = event.getX() - mDownPositionX; - final float deltaY = event.getY() - mDownPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - final ViewConfiguration viewConfiguration = ViewConfiguration.get( - TextView.this.getContext()); - int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); - - if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { - mGestureStayedInTapRegion = false; - } - } - break; - - case MotionEvent.ACTION_UP: - mPreviousTapUpTime = SystemClock.uptimeMillis(); - break; - } - } - - /** - * @param event - */ - private void updateMinAndMaxOffsets(MotionEvent event) { - int pointerCount = event.getPointerCount(); - for (int index = 0; index < pointerCount; index++) { - int offset = getOffsetForPosition(event.getX(index), event.getY(index)); - if (offset < mMinTouchOffset) mMinTouchOffset = offset; - if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; - } - } - - public int getMinTouchOffset() { - return mMinTouchOffset; - } - - public int getMaxTouchOffset() { - return mMaxTouchOffset; - } - - public void resetTouchOffsets() { - mMinTouchOffset = mMaxTouchOffset = -1; - } - - /** - * @return true iff this controller is currently used to move the selection start. - */ - public boolean isSelectionStartDragged() { - return mStartHandle != null && mStartHandle.isDragging(); - } - - public void onTouchModeChanged(boolean isInTouchMode) { - if (!isInTouchMode) { - hide(); - } - } - - @Override - public void onDetached() { - final ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnTouchModeChangeListener(this); - - if (mStartHandle != null) mStartHandle.onDetached(); - if (mEndHandle != null) mEndHandle.onDetached(); - } - } - - static class InputContentType { - int imeOptions = EditorInfo.IME_NULL; - String privateImeOptions; - CharSequence imeActionLabel; - int imeActionId; - Bundle extras; - OnEditorActionListener onEditorActionListener; - boolean enterDown; - } - - static class InputMethodState { - Rect mCursorRectInWindow = new Rect(); - RectF mTmpRectF = new RectF(); - float[] mTmpOffset = new float[2]; - ExtractedTextRequest mExtracting; - final ExtractedText mTmpExtracted = new ExtractedText(); - int mBatchEditNesting; - boolean mCursorChanged; - boolean mSelectionModeChanged; - boolean mContentChanged; - int mChangedStart, mChangedEnd, mChangedDelta; - } - - private class Editor { - // Cursor Controllers. - InsertionPointCursorController mInsertionPointCursorController; - SelectionModifierCursorController mSelectionModifierCursorController; - ActionMode mSelectionActionMode; - boolean mInsertionControllerEnabled; - boolean mSelectionControllerEnabled; - - // Used to highlight a word when it is corrected by the IME - CorrectionHighlighter mCorrectionHighlighter; - - InputContentType mInputContentType; - InputMethodState mInputMethodState; - - DisplayList[] mTextDisplayLists; - - boolean mFrozenWithFocus; - boolean mSelectionMoved; - boolean mTouchFocusSelected; - - KeyListener mKeyListener; - int mInputType = EditorInfo.TYPE_NULL; - - boolean mDiscardNextActionUp; - boolean mIgnoreActionUpEvent; - - long mShowCursor; - Blink mBlink; - - boolean mCursorVisible = true; - boolean mSelectAllOnFocus; - boolean mTextIsSelectable; - - CharSequence mError; - boolean mErrorWasChanged; - ErrorPopup mErrorPopup; - /** - * This flag is set if the TextView tries to display an error before it - * is attached to the window (so its position is still unknown). - * It causes the error to be shown later, when onAttachedToWindow() - * is called. - */ - boolean mShowErrorAfterAttach; - - boolean mInBatchEditControllers; - - SuggestionsPopupWindow mSuggestionsPopupWindow; - SuggestionRangeSpan mSuggestionRangeSpan; - Runnable mShowSuggestionRunnable; - - final Drawable[] mCursorDrawable = new Drawable[2]; - int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split) - - Drawable mSelectHandleLeft; - Drawable mSelectHandleRight; - Drawable mSelectHandleCenter; - - // Global listener that detects changes in the global position of the TextView - PositionListener mPositionListener; - - float mLastDownPositionX, mLastDownPositionY; - Callback mCustomSelectionActionModeCallback; - - // Set when this TextView gained focus with some text selected. Will start selection mode. - boolean mCreatedWithASelection; - - WordIterator mWordIterator; - SpellChecker mSpellChecker; - - void onAttachedToWindow() { - final ViewTreeObserver observer = getViewTreeObserver(); - // No need to create the controller. - // The get method will add the listener on controller creation. - if (mInsertionPointCursorController != null) { - observer.addOnTouchModeChangeListener(mInsertionPointCursorController); - } - if (mSelectionModifierCursorController != null) { - observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); - } - updateSpellCheckSpans(0, mText.length(), true /* create the spell checker if needed */); - } - - void onDetachedFromWindow() { - if (mError != null) { - hideError(); - } - - if (mBlink != null) { - mBlink.removeCallbacks(mBlink); - } - - if (mInsertionPointCursorController != null) { - mInsertionPointCursorController.onDetached(); - } - - if (mSelectionModifierCursorController != null) { - mSelectionModifierCursorController.onDetached(); - } - - if (mShowSuggestionRunnable != null) { - removeCallbacks(mShowSuggestionRunnable); - } - - invalidateTextDisplayList(); - - if (mSpellChecker != null) { - mSpellChecker.closeSession(); - // Forces the creation of a new SpellChecker next time this window is created. - // Will handle the cases where the settings has been changed in the meantime. - mSpellChecker = null; - } - - hideControllers(); - } - - void onScreenStateChanged(int screenState) { - switch (screenState) { - case SCREEN_STATE_ON: - resumeBlink(); - break; - case SCREEN_STATE_OFF: - suspendBlink(); - break; - } - } - - private void suspendBlink() { - if (mBlink != null) { - mBlink.cancel(); - } - } - - private void resumeBlink() { - if (mBlink != null) { - mBlink.uncancel(); - makeBlink(); - } - } - - void adjustInputType(boolean password, boolean passwordInputType, - boolean webPasswordInputType, boolean numberPasswordInputType) { - // mInputType has been set from inputType, possibly modified by mInputMethod. - // Specialize mInputType to [web]password if we have a text class and the original input - // type was a password. - if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { - if (password || passwordInputType) { - mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) - | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; - } - if (webPasswordInputType) { - mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) - | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; - } - } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) { - if (numberPasswordInputType) { - mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) - | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD; - } - } - } - - void setFrame() { - if (mErrorPopup != null) { - TextView tv = (TextView) mErrorPopup.getContentView(); - chooseSize(mErrorPopup, mError, tv); - mErrorPopup.update(TextView.this, getErrorX(), getErrorY(), - mErrorPopup.getWidth(), mErrorPopup.getHeight()); - } - } - - void onFocusChanged(boolean focused, int direction) { - mShowCursor = SystemClock.uptimeMillis(); - ensureEndedBatchEdit(); - - if (focused) { - int selStart = getSelectionStart(); - int selEnd = getSelectionEnd(); - - // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection - // mode for these, unless there was a specific selection already started. - final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 && - selEnd == mText.length(); - - mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted; - - if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { - // If a tap was used to give focus to that view, move cursor at tap position. - // Has to be done before onTakeFocus, which can be overloaded. - final int lastTapPosition = getLastTapPosition(); - if (lastTapPosition >= 0) { - Selection.setSelection((Spannable) mText, lastTapPosition); - } - - // Note this may have to be moved out of the Editor class - if (mMovement != null) { - mMovement.onTakeFocus(TextView.this, (Spannable) mText, direction); - } - - // The DecorView does not have focus when the 'Done' ExtractEditText button is - // pressed. Since it is the ViewAncestor's mView, it requests focus before - // ExtractEditText clears focus, which gives focus to the ExtractEditText. - // This special case ensure that we keep current selection in that case. - // It would be better to know why the DecorView does not have focus at that time. - if (((TextView.this instanceof ExtractEditText) || mSelectionMoved) && - selStart >= 0 && selEnd >= 0) { - /* - * Someone intentionally set the selection, so let them - * do whatever it is that they wanted to do instead of - * the default on-focus behavior. We reset the selection - * here instead of just skipping the onTakeFocus() call - * because some movement methods do something other than - * just setting the selection in theirs and we still - * need to go through that path. - */ - Selection.setSelection((Spannable) mText, selStart, selEnd); - } - - if (mSelectAllOnFocus) { - selectAll(); - } - - mTouchFocusSelected = true; - } - - mFrozenWithFocus = false; - mSelectionMoved = false; - - if (mError != null) { - showError(); - } - - makeBlink(); - } else { - if (mError != null) { - hideError(); - } - // Don't leave us in the middle of a batch edit. - onEndBatchEdit(); - - if (TextView.this instanceof ExtractEditText) { - // terminateTextSelectionMode removes selection, which we want to keep when - // ExtractEditText goes out of focus. - final int selStart = getSelectionStart(); - final int selEnd = getSelectionEnd(); - hideControllers(); - Selection.setSelection((Spannable) mText, selStart, selEnd); - } else { - hideControllers(); - downgradeEasyCorrectionSpans(); - } - - // No need to create the controller - if (mSelectionModifierCursorController != null) { - mSelectionModifierCursorController.resetTouchOffsets(); - } - } - } - - void sendOnTextChanged(int start, int after) { - updateSpellCheckSpans(start, start + after, false); - - // Hide the controllers as soon as text is modified (typing, procedural...) - // We do not hide the span controllers, since they can be added when a new text is - // inserted into the text view (voice IME). - hideCursorControllers(); - } - - private int getLastTapPosition() { - // No need to create the controller at that point, no last tap position saved - if (mSelectionModifierCursorController != null) { - int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset(); - if (lastTapPosition >= 0) { - // Safety check, should not be possible. - if (lastTapPosition > mText.length()) { - Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs " - + mText.length() + ")"); - lastTapPosition = mText.length(); - } - return lastTapPosition; - } - } - - return -1; - } - - void onWindowFocusChanged(boolean hasWindowFocus) { - if (hasWindowFocus) { - if (mBlink != null) { - mBlink.uncancel(); - makeBlink(); - } - } else { - if (mBlink != null) { - mBlink.cancel(); - } - if (mInputContentType != null) { - mInputContentType.enterDown = false; - } - // Order matters! Must be done before onParentLostFocus to rely on isShowingUp - hideControllers(); - if (mSuggestionsPopupWindow != null) { - mSuggestionsPopupWindow.onParentLostFocus(); - } - - // Don't leave us in the middle of a batch edit. - onEndBatchEdit(); - } - } - - void onTouchEvent(MotionEvent event) { - if (hasSelectionController()) { - getSelectionController().onTouchEvent(event); - } - - if (mShowSuggestionRunnable != null) { - removeCallbacks(mShowSuggestionRunnable); - mShowSuggestionRunnable = null; - } - - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mLastDownPositionX = event.getX(); - mLastDownPositionY = event.getY(); - - // Reset this state; it will be re-set if super.onTouchEvent - // causes focus to move to the view. - mTouchFocusSelected = false; - mIgnoreActionUpEvent = false; - } - } - - void onDraw(Canvas canvas, Layout layout, Path highlight, int cursorOffsetVertical) { - final int selectionStart = getSelectionStart(); - final int selectionEnd = getSelectionEnd(); - - final InputMethodState ims = mInputMethodState; - if (ims != null && ims.mBatchEditNesting == 0) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - if (imm.isActive(TextView.this)) { - boolean reported = false; - if (ims.mContentChanged || ims.mSelectionModeChanged) { - // We are in extract mode and the content has changed - // in some way... just report complete new text to the - // input method. - reported = reportExtractedText(); - } - if (!reported && highlight != null) { - int candStart = -1; - int candEnd = -1; - if (mText instanceof Spannable) { - Spannable sp = (Spannable)mText; - candStart = EditableInputConnection.getComposingSpanStart(sp); - candEnd = EditableInputConnection.getComposingSpanEnd(sp); - } - imm.updateSelection(TextView.this, - selectionStart, selectionEnd, candStart, candEnd); - } - } - - if (imm.isWatchingCursor(TextView.this) && highlight != null) { - highlight.computeBounds(ims.mTmpRectF, true); - ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0; - - canvas.getMatrix().mapPoints(ims.mTmpOffset); - ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]); - - ims.mTmpRectF.offset(0, cursorOffsetVertical); - - ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5), - (int)(ims.mTmpRectF.top + 0.5), - (int)(ims.mTmpRectF.right + 0.5), - (int)(ims.mTmpRectF.bottom + 0.5)); - - imm.updateCursor(TextView.this, - ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top, - ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom); - } - } - } - - if (mCorrectionHighlighter != null) { - mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); - } - - if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) { - drawCursor(canvas, cursorOffsetVertical); - // Rely on the drawable entirely, do not draw the cursor line. - // Has to be done after the IMM related code above which relies on the highlight. - highlight = null; - } - - if (canHaveDisplayList() && canvas.isHardwareAccelerated()) { - drawHardwareAccelerated(canvas, layout, highlight, cursorOffsetVertical); - } else { - layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); - } - - if (mMarquee != null && mMarquee.shouldDrawGhost()) { - canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); - layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); - } - } - - private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, - int cursorOffsetVertical) { - final int width = mRight - mLeft; - final int height = mBottom - mTop; - - final long lineRange = layout.getLineRangeForDraw(canvas); - int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); - int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); - if (lastLine < 0) return; - - layout.drawBackground(canvas, highlight, mHighlightPaint, cursorOffsetVertical, - firstLine, lastLine); - - if (layout instanceof DynamicLayout) { - if (mTextDisplayLists == null) { - mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)]; - } - - DynamicLayout dynamicLayout = (DynamicLayout) layout; - int[] blockEnds = dynamicLayout.getBlockEnds(); - int[] blockIndices = dynamicLayout.getBlockIndices(); - final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); - - canvas.translate(mScrollX, mScrollY); - int endOfPreviousBlock = -1; - int searchStartIndex = 0; - for (int i = 0; i < numberOfBlocks; i++) { - int blockEnd = blockEnds[i]; - int blockIndex = blockIndices[i]; - - final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX; - if (blockIsInvalid) { - blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks, - searchStartIndex); - // Dynamic layout internal block indices structure is updated from Editor - blockIndices[i] = blockIndex; - searchStartIndex = blockIndex + 1; - } - - DisplayList blockDisplayList = mTextDisplayLists[blockIndex]; - if (blockDisplayList == null) { - blockDisplayList = mTextDisplayLists[blockIndex] = - getHardwareRenderer().createDisplayList("Text " + blockIndex); - } else { - if (blockIsInvalid) blockDisplayList.invalidate(); - } - - if (!blockDisplayList.isValid()) { - final HardwareCanvas hardwareCanvas = blockDisplayList.start(); - try { - hardwareCanvas.setViewport(width, height); - // The dirty rect should always be null for a display list - hardwareCanvas.onPreDraw(null); - hardwareCanvas.translate(-mScrollX, -mScrollY); - layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd); - hardwareCanvas.translate(mScrollX, mScrollY); - } finally { - hardwareCanvas.onPostDraw(); - blockDisplayList.end(); - if (USE_DISPLAY_LIST_PROPERTIES) { - blockDisplayList.setLeftTopRightBottom(0, 0, width, height); - } - } - } - - ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null, - DisplayList.FLAG_CLIP_CHILDREN); - endOfPreviousBlock = blockEnd; - } - canvas.translate(-mScrollX, -mScrollY); - } else { - // Fallback on the layout method (a BoringLayout is used when the text is empty) - layout.drawText(canvas, firstLine, lastLine); - } - } - - private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks, - int searchStartIndex) { - int length = mTextDisplayLists.length; - for (int i = searchStartIndex; i < length; i++) { - boolean blockIndexFound = false; - for (int j = 0; j < numberOfBlocks; j++) { - if (blockIndices[j] == i) { - blockIndexFound = true; - break; - } - } - if (blockIndexFound) continue; - return i; - } - - // No available index found, the pool has to grow - int newSize = ArrayUtils.idealIntArraySize(length + 1); - DisplayList[] displayLists = new DisplayList[newSize]; - System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length); - mTextDisplayLists = displayLists; - return length; - } - - private void drawCursor(Canvas canvas, int cursorOffsetVertical) { - final boolean translate = cursorOffsetVertical != 0; - if (translate) canvas.translate(0, cursorOffsetVertical); - for (int i = 0; i < getEditor().mCursorCount; i++) { - mCursorDrawable[i].draw(canvas); - } - if (translate) canvas.translate(0, -cursorOffsetVertical); - } - - private void invalidateTextDisplayList() { - if (mTextDisplayLists != null) { - for (int i = 0; i < mTextDisplayLists.length; i++) { - if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate(); - } - } - } - - private void updateCursorsPositions() { - if (mCursorDrawableRes == 0) { - mCursorCount = 0; - return; - } - - final int offset = getSelectionStart(); - final int line = mLayout.getLineForOffset(offset); - final int top = mLayout.getLineTop(line); - final int bottom = mLayout.getLineTop(line + 1); - - mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1; - - int middle = bottom; - if (mCursorCount == 2) { - // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)} - middle = (top + bottom) >> 1; - } - - updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset)); - - if (mCursorCount == 2) { - updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset)); - } - } - - private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { - if (mCursorDrawable[cursorIndex] == null) - mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes); - - if (mTempRect == null) mTempRect = new Rect(); - mCursorDrawable[cursorIndex].getPadding(mTempRect); - final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth(); - horizontal = Math.max(0.5f, horizontal - 0.5f); - final int left = (int) (horizontal) - mTempRect.left; - mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width, - bottom + mTempRect.bottom); - } } } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 5a7d5195d0d9..93f90f6fa175 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -438,10 +438,9 @@ public class LockPatternUtils { * Calls back SetupFaceLock to delete the temporary gallery file */ public void deleteTempGallery() { - Intent intent = new Intent().setClassName("com.android.facelock", - "com.android.facelock.SetupFaceLock"); + Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY"); intent.putExtra("deleteTempGallery", true); - mContext.startActivity(intent); + mContext.sendBroadcast(intent); } /** @@ -449,10 +448,9 @@ public class LockPatternUtils { */ void deleteGallery() { if(usingBiometricWeak()) { - Intent intent = new Intent().setClassName("com.android.facelock", - "com.android.facelock.SetupFaceLock"); + Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY"); intent.putExtra("deleteGallery", true); - mContext.startActivity(intent); + mContext.sendBroadcast(intent); } } diff --git a/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java index 6a471add46d9..b2075aeec54d 100644 --- a/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java +++ b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java @@ -16,12 +16,8 @@ package android.app; -import coretestutils.http.MockResponse; -import coretestutils.http.MockWebServer; - import android.app.DownloadManager.Query; import android.app.DownloadManager.Request; -import android.app.DownloadManagerBaseTest.DataType; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -39,10 +35,14 @@ import android.provider.Settings; import android.test.InstrumentationTestCase; import android.util.Log; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; + import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; @@ -53,13 +53,15 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.TimeoutException; +import libcore.io.Streams; + /** * Base class for Instrumented tests for the Download Manager. */ public class DownloadManagerBaseTest extends InstrumentationTestCase { private static final String TAG = "DownloadManagerBaseTest"; protected DownloadManager mDownloadManager = null; - protected MockWebServer mServer = null; + private MockWebServer mServer = null; protected String mFileType = "text/plain"; protected Context mContext = null; protected MultipleDownloadsCompletedReceiver mReceiver = null; @@ -237,63 +239,57 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { mContext = getInstrumentation().getContext(); mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE); mServer = new MockWebServer(); + mServer.play(); mReceiver = registerNewMultipleDownloadsReceiver(); // Note: callers overriding this should call mServer.play() with the desired port # } /** - * Helper to enqueue a response from the MockWebServer with no body. + * Helper to build a response from the MockWebServer with no body. * * @param status The HTTP status code to return for this response * @return Returns the mock web server response that was queued (which can be modified) */ - protected MockResponse enqueueResponse(int status) { - return doEnqueueResponse(status); - + protected MockResponse buildResponse(int status) { + MockResponse response = new MockResponse().setResponseCode(status); + response.setHeader("Content-type", mFileType); + return response; } /** - * Helper to enqueue a response from the MockWebServer. + * Helper to build a response from the MockWebServer. * * @param status The HTTP status code to return for this response * @param body The body to return in this response * @return Returns the mock web server response that was queued (which can be modified) */ - protected MockResponse enqueueResponse(int status, byte[] body) { - return doEnqueueResponse(status).setBody(body); - + protected MockResponse buildResponse(int status, byte[] body) { + return buildResponse(status).setBody(body); } /** - * Helper to enqueue a response from the MockWebServer. + * Helper to build a response from the MockWebServer. * * @param status The HTTP status code to return for this response * @param bodyFile The body to return in this response * @return Returns the mock web server response that was queued (which can be modified) */ - protected MockResponse enqueueResponse(int status, File bodyFile) { - return doEnqueueResponse(status).setBody(bodyFile); + protected MockResponse buildResponse(int status, File bodyFile) + throws FileNotFoundException, IOException { + final byte[] body = Streams.readFully(new FileInputStream(bodyFile)); + return buildResponse(status).setBody(body); } - /** - * Helper for enqueue'ing a response from the MockWebServer. - * - * @param status The HTTP status code to return for this response - * @return Returns the mock web server response that was queued (which can be modified) - */ - protected MockResponse doEnqueueResponse(int status) { - MockResponse response = new MockResponse().setResponseCode(status); - response.addHeader("Content-type", mFileType); - mServer.enqueue(response); - return response; + protected void enqueueResponse(MockResponse resp) { + mServer.enqueue(resp); } /** * Helper to generate a random blob of bytes. * * @param size The size of the data to generate - * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or - * {@link DataType.BINARY}. + * @param type The type of data to generate: currently, one of {@link DataType#TEXT} or + * {@link DataType#BINARY}. * @return The random data that is generated. */ protected byte[] generateData(int size, DataType type) { @@ -304,8 +300,8 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { * Helper to generate a random blob of bytes using a given RNG. * * @param size The size of the data to generate - * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or - * {@link DataType.BINARY}. + * @param type The type of data to generate: currently, one of {@link DataType#TEXT} or + * {@link DataType#BINARY}. * @param rng (optional) The RNG to use; pass null to use * @return The random data that is generated. */ @@ -492,8 +488,6 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { assertEquals(1, cursor.getCount()); assertTrue(cursor.moveToFirst()); - mServer.checkForExceptions(); - verifyFileSize(pfd, fileSize); verifyFileContents(pfd, fileData); } finally { @@ -928,7 +922,7 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { protected long enqueueDownloadRequest(byte[] body, int location) throws Exception { // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, body); + mServer.enqueue(buildResponse(HTTP_OK, body)); return doEnqueue(location); } @@ -943,7 +937,7 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { protected long enqueueDownloadRequest(File body, int location) throws Exception { // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, body); + mServer.enqueue(buildResponse(HTTP_OK, body)); return doEnqueue(location); } @@ -1035,4 +1029,4 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { assertEquals(1, mReceiver.numDownloadsCompleted()); return dlRequest; } -}
\ No newline at end of file +} diff --git a/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java b/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java index afe7f5566a44..aa9f69dc84b1 100644 --- a/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java +++ b/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java @@ -16,8 +16,6 @@ package android.app; -import coretestutils.http.MockResponse; - import android.app.DownloadManager.Query; import android.app.DownloadManager.Request; import android.database.Cursor; @@ -26,6 +24,8 @@ import android.os.Environment; import android.os.ParcelFileDescriptor; import android.test.suitebuilder.annotation.LargeTest; +import com.google.mockwebserver.MockResponse; + import java.io.File; import java.util.Iterator; import java.util.Set; @@ -47,7 +47,6 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { public void setUp() throws Exception { super.setUp(); setWiFiStateOn(true); - mServer.play(); removeAllCurrentDownloads(); } @@ -132,8 +131,6 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { assertEquals(1, cursor.getCount()); assertTrue(cursor.moveToFirst()); - mServer.checkForExceptions(); - verifyFileSize(pfd, fileSize); verifyFileContents(pfd, fileData); int colIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME); @@ -154,7 +151,7 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, blobData); + enqueueResponse(buildResponse(HTTP_OK, blobData)); try { Uri uri = getServerUri(DEFAULT_FILENAME); @@ -193,7 +190,7 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, blobData); + enqueueResponse(buildResponse(HTTP_OK, blobData)); Uri uri = getServerUri(DEFAULT_FILENAME); Request request = new Request(uri); @@ -224,7 +221,7 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, blobData); + enqueueResponse(buildResponse(HTTP_OK, blobData)); Uri uri = getServerUri(DEFAULT_FILENAME); Request request = new Request(uri); @@ -251,7 +248,7 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { public void testGetDownloadIdOnNotification() throws Exception { byte[] blobData = generateData(3000, DataType.TEXT); // file size = 3000 bytes - MockResponse response = enqueueResponse(HTTP_OK, blobData); + enqueueResponse(buildResponse(HTTP_OK, blobData)); long dlRequest = doCommonStandardEnqueue(); waitForDownloadOrTimeout(dlRequest); @@ -271,8 +268,9 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { // force 6 redirects for (int i = 0; i < 6; ++i) { - MockResponse response = enqueueResponse(HTTP_REDIRECT); - response.addHeader("Location", uri.toString()); + final MockResponse resp = buildResponse(HTTP_REDIRECT); + resp.setHeader("Location", uri.toString()); + enqueueResponse(resp); } doErrorTest(uri, DownloadManager.ERROR_TOO_MANY_REDIRECTS); } @@ -283,7 +281,7 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { @LargeTest public void testErrorUnhandledHttpCode() throws Exception { Uri uri = getServerUri(DEFAULT_FILENAME); - MockResponse response = enqueueResponse(HTTP_PARTIAL_CONTENT); + enqueueResponse(buildResponse(HTTP_PARTIAL_CONTENT)); doErrorTest(uri, DownloadManager.ERROR_UNHANDLED_HTTP_CODE); } @@ -294,8 +292,9 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { @LargeTest public void testErrorHttpDataError_invalidRedirect() throws Exception { Uri uri = getServerUri(DEFAULT_FILENAME); - MockResponse response = enqueueResponse(HTTP_REDIRECT); - response.addHeader("Location", "://blah.blah.blah.com"); + final MockResponse resp = buildResponse(HTTP_REDIRECT); + resp.setHeader("Location", "://blah.blah.blah.com"); + enqueueResponse(resp); doErrorTest(uri, DownloadManager.ERROR_HTTP_DATA_ERROR); } @@ -327,7 +326,7 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { public void testSetTitle() throws Exception { int fileSize = 1024; byte[] blobData = generateData(fileSize, DataType.BINARY); - MockResponse response = enqueueResponse(HTTP_OK, blobData); + enqueueResponse(buildResponse(HTTP_OK, blobData)); // An arbitrary unicode string title final String title = "\u00a5123;\"\u0152\u017d \u054b \u0a07 \ucce0 \u6820\u03a8\u5c34" + @@ -359,7 +358,7 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { byte[] blobData = generateData(fileSize, DataType.TEXT); setWiFiStateOn(false); - enqueueResponse(HTTP_OK, blobData); + enqueueResponse(buildResponse(HTTP_OK, blobData)); try { Uri uri = getServerUri(DEFAULT_FILENAME); @@ -383,32 +382,16 @@ public class DownloadManagerFunctionalTest extends DownloadManagerBaseTest { } /** - * Tests when the server drops the connection after all headers (but before any data send). - */ - @LargeTest - public void testDropConnection_headers() throws Exception { - byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); - - MockResponse response = enqueueResponse(HTTP_OK, blobData); - response.setCloseConnectionAfterHeader("content-length"); - long dlRequest = doCommonStandardEnqueue(); - - // Download will never complete when header is dropped - boolean success = waitForDownloadOrTimeoutNoThrow(dlRequest, DEFAULT_WAIT_POLL_TIME, - DEFAULT_MAX_WAIT_TIME); - - assertFalse(success); - } - - /** * Tests that we get an error code when the server drops the connection during a download. */ @LargeTest public void testServerDropConnection_body() throws Exception { byte[] blobData = generateData(25000, DataType.TEXT); // file size = 25000 bytes - MockResponse response = enqueueResponse(HTTP_OK, blobData); - response.setCloseConnectionAfterXBytes(15382); + final MockResponse resp = buildResponse(HTTP_OK, blobData); + resp.setHeader("Content-Length", "50000"); + enqueueResponse(resp); + long dlRequest = doCommonStandardEnqueue(); waitForDownloadOrTimeout(dlRequest); diff --git a/core/tests/coretests/src/android/app/DownloadManagerStressTest.java b/core/tests/coretests/src/android/app/DownloadManagerStressTest.java index bdeb5544fad9..864b2d6ebce1 100644 --- a/core/tests/coretests/src/android/app/DownloadManagerStressTest.java +++ b/core/tests/coretests/src/android/app/DownloadManagerStressTest.java @@ -46,7 +46,6 @@ public class DownloadManagerStressTest extends DownloadManagerBaseTest { public void setUp() throws Exception { super.setUp(); setWiFiStateOn(true); - mServer.play(); removeAllCurrentDownloads(); } @@ -85,7 +84,7 @@ public class DownloadManagerStressTest extends DownloadManagerBaseTest { request.setTitle(String.format("%s--%d", DEFAULT_FILENAME + i, i)); // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, blobData); + enqueueResponse(buildResponse(HTTP_OK, blobData)); long requestID = mDownloadManager.enqueue(request); } @@ -127,7 +126,7 @@ public class DownloadManagerStressTest extends DownloadManagerBaseTest { try { long dlRequest = doStandardEnqueue(largeFile); - // wait for the download to complete + // wait for the download to complete waitForDownloadOrTimeout(dlRequest); ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest); diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk index a419068c633c..09dcac5f1547 100644 --- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk @@ -20,7 +20,7 @@ LOCAL_MODULE_TAGS := tests LOCAL_SRC_FILES := $(call all-java-files-under, src) -LOCAL_STATIC_JAVA_LIBRARIES := android-common frameworks-core-util-lib +LOCAL_STATIC_JAVA_LIBRARIES := android-common mockwebserver LOCAL_SDK_VERSION := current LOCAL_PACKAGE_NAME := DownloadManagerTestApp diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java index 334661d9c697..8e935f83fae9 100644 --- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java @@ -18,7 +18,6 @@ package com.android.frameworks.downloadmanagertests; import android.app.DownloadManager; import android.app.DownloadManager.Query; -import android.app.DownloadManager.Request; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -26,37 +25,19 @@ import android.content.IntentFilter; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.Uri; import android.net.wifi.WifiManager; -import android.os.Bundle; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.SystemClock; -import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.provider.Settings; import android.test.InstrumentationTestCase; import android.util.Log; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URL; -import java.util.concurrent.TimeoutException; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Random; import java.util.Set; -import java.util.Vector; - -import junit.framework.AssertionFailedError; - -import coretestutils.http.MockResponse; -import coretestutils.http.MockWebServer; +import java.util.concurrent.TimeoutException; /** * Base class for Instrumented tests for the Download Manager. @@ -64,7 +45,6 @@ import coretestutils.http.MockWebServer; public class DownloadManagerBaseTest extends InstrumentationTestCase { protected DownloadManager mDownloadManager = null; - protected MockWebServer mServer = null; protected String mFileType = "text/plain"; protected Context mContext = null; protected MultipleDownloadsCompletedReceiver mReceiver = null; @@ -77,7 +57,6 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { protected static final int HTTP_PARTIAL_CONTENT = 206; protected static final int HTTP_NOT_FOUND = 404; protected static final int HTTP_SERVICE_UNAVAILABLE = 503; - protected String DEFAULT_FILENAME = "somefile.txt"; protected static final int DEFAULT_MAX_WAIT_TIME = 2 * 60 * 1000; // 2 minutes protected static final int DEFAULT_WAIT_POLL_TIME = 5 * 1000; // 5 seconds @@ -86,48 +65,6 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { protected static final int MAX_WAIT_FOR_DOWNLOAD_TIME = 5 * 60 * 1000; // 5 minutes protected static final int MAX_WAIT_FOR_LARGE_DOWNLOAD_TIME = 15 * 60 * 1000; // 15 minutes - protected static final int DOWNLOAD_TO_SYSTEM_CACHE = 1; - protected static final int DOWNLOAD_TO_DOWNLOAD_CACHE_DIR = 2; - - // Just a few popular file types used to return from a download - protected enum DownloadFileType { - PLAINTEXT, - APK, - GIF, - GARBAGE, - UNRECOGNIZED, - ZIP - } - - protected enum DataType { - TEXT, - BINARY - } - - public static class LoggingRng extends Random { - - /** - * Constructor - * - * Creates RNG with self-generated seed value. - */ - public LoggingRng() { - this(SystemClock.uptimeMillis()); - } - - /** - * Constructor - * - * Creats RNG with given initial seed value - - * @param seed The initial seed value - */ - public LoggingRng(long seed) { - super(seed); - Log.i(LOG_TAG, "Seeding RNG with value: " + seed); - } - } - public static class MultipleDownloadsCompletedReceiver extends BroadcastReceiver { private volatile int mNumDownloadsCompleted = 0; private Set<Long> downloadIds = Collections.synchronizedSet(new HashSet<Long>()); @@ -171,7 +108,7 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { /** * Gets the number of times the {@link #onReceive} callback has been called for the - * {@link DownloadManager.ACTION_DOWNLOAD_COMPLETED} action, indicating the number of + * {@link DownloadManager#ACTION_DOWNLOAD_COMPLETE} action, indicating the number of * downloads completed thus far. * * @return the number of downloads completed so far. @@ -241,76 +178,7 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { public void setUp() throws Exception { mContext = getInstrumentation().getContext(); mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE); - mServer = new MockWebServer(); mReceiver = registerNewMultipleDownloadsReceiver(); - // Note: callers overriding this should call mServer.play() with the desired port # - } - - /** - * Helper to enqueue a response from the MockWebServer. - * - * @param status The HTTP status code to return for this response - * @param body The body to return in this response - * @return Returns the mock web server response that was queued (which can be modified) - */ - private MockResponse enqueueResponse(int status, byte[] body) { - return doEnqueueResponse(status).setBody(body); - - } - - /** - * Helper to enqueue a response from the MockWebServer. - * - * @param status The HTTP status code to return for this response - * @param bodyFile The body to return in this response - * @return Returns the mock web server response that was queued (which can be modified) - */ - private MockResponse enqueueResponse(int status, File bodyFile) { - return doEnqueueResponse(status).setBody(bodyFile); - } - - /** - * Helper for enqueue'ing a response from the MockWebServer. - * - * @param status The HTTP status code to return for this response - * @return Returns the mock web server response that was queued (which can be modified) - */ - private MockResponse doEnqueueResponse(int status) { - MockResponse response = new MockResponse().setResponseCode(status); - response.addHeader("Content-type", mFileType); - mServer.enqueue(response); - return response; - } - - /** - * Helper to generate a random blob of bytes using a given RNG. - * - * @param size The size of the data to generate - * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or - * {@link DataType.BINARY}. - * @param rng (optional) The RNG to use; pass null to use - * @return The random data that is generated. - */ - private byte[] generateData(int size, DataType type, Random rng) { - int min = Byte.MIN_VALUE; - int max = Byte.MAX_VALUE; - - // Only use chars in the HTTP ASCII printable character range for Text - if (type == DataType.TEXT) { - min = 32; - max = 126; - } - byte[] result = new byte[size]; - Log.i(LOG_TAG, "Generating data of size: " + size); - - if (rng == null) { - rng = new LoggingRng(); - } - - for (int i = 0; i < size; ++i) { - result[i] = (byte) (min + rng.nextInt(max - min + 1)); - } - return result; } /** @@ -324,76 +192,6 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { } /** - * Helper to verify the contents of a downloaded file versus a byte[]. - * - * @param actual The file of whose contents to verify - * @param expected The data we expect to find in the aforementioned file - * @throws IOException if there was a problem reading from the file - */ - private void verifyFileContents(ParcelFileDescriptor actual, byte[] expected) - throws IOException { - AutoCloseInputStream input = new ParcelFileDescriptor.AutoCloseInputStream(actual); - long fileSize = actual.getStatSize(); - - assertTrue(fileSize <= Integer.MAX_VALUE); - assertEquals(expected.length, fileSize); - - byte[] actualData = new byte[expected.length]; - assertEquals(input.read(actualData), fileSize); - compareByteArrays(actualData, expected); - } - - /** - * Helper to compare 2 byte arrays. - * - * @param actual The array whose data we want to verify - * @param expected The array of data we expect to see - */ - private void compareByteArrays(byte[] actual, byte[] expected) { - assertEquals(actual.length, expected.length); - int length = actual.length; - for (int i = 0; i < length; ++i) { - // assert has a bit of overhead, so only do the assert when the values are not the same - if (actual[i] != expected[i]) { - fail("Byte arrays are not equal."); - } - } - } - - /** - * Gets the MIME content string for a given type - * - * @param type The MIME type to return - * @return the String representation of that MIME content type - */ - protected String getMimeMapping(DownloadFileType type) { - switch (type) { - case APK: - return "application/vnd.android.package-archive"; - case GIF: - return "image/gif"; - case ZIP: - return "application/x-zip-compressed"; - case GARBAGE: - return "zip\\pidy/doo/da"; - case UNRECOGNIZED: - return "application/new.undefined.type.of.app"; - } - return "text/plain"; - } - - /** - * Gets the Uri that should be used to access the mock server - * - * @param filename The name of the file to try to retrieve from the mock server - * @return the Uri to use for access the file on the mock server - */ - private Uri getServerUri(String filename) throws Exception { - URL url = mServer.getUrl("/" + filename); - return Uri.parse(url.toString()); - } - - /** * Helper to create and register a new MultipleDownloadCompletedReciever * * This is used to track many simultaneous downloads by keeping count of all the downloads @@ -738,39 +536,6 @@ public class DownloadManagerBaseTest extends InstrumentationTestCase { } /** - * Helper to perform a standard enqueue of data to the mock server. - * download is performed to the downloads cache dir (NOT systemcache dir) - * - * @param body The body to return in the response from the server - */ - private long doStandardEnqueue(byte[] body) throws Exception { - // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, body); - return doCommonStandardEnqueue(); - } - - /** - * Helper to perform a standard enqueue of data to the mock server. - * - * @param body The body to return in the response from the server, contained in the file - */ - private long doStandardEnqueue(File body) throws Exception { - // Prepare the mock server with a standard response - enqueueResponse(HTTP_OK, body); - return doCommonStandardEnqueue(); - } - - /** - * Helper to do the additional steps (setting title and Uri of default filename) when - * doing a standard enqueue request to the server. - */ - private long doCommonStandardEnqueue() throws Exception { - Uri uri = getServerUri(DEFAULT_FILENAME); - Request request = new Request(uri).setTitle(DEFAULT_FILENAME); - return mDownloadManager.enqueue(request); - } - - /** * Helper to verify an int value in a Cursor * * @param cursor The cursor containing the query results diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java index 654f74794ac3..9c44d6117dfe 100644 --- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java @@ -16,16 +16,11 @@ package com.android.frameworks.downloadmanagertests; import android.app.DownloadManager; -import android.app.DownloadManager.Query; import android.app.DownloadManager.Request; -import android.content.Context; -import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Environment; import android.os.ParcelFileDescriptor; -import android.provider.Settings; -import android.test.suitebuilder.annotation.LargeTest; import android.util.Log; import java.io.DataInputStream; @@ -33,13 +28,8 @@ import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileWriter; import java.util.HashSet; -import coretestutils.http.MockResponse; -import coretestutils.http.MockWebServer; -import coretestutils.http.RecordedRequest; - /** * Class to test downloading files from a real (not mock) external server. */ @@ -243,7 +233,7 @@ public class DownloadManagerTestApp extends DownloadManagerBaseTest { Uri remoteUri = getExternalFileUri(filename); Request request = new Request(remoteUri); - request.setMimeType(getMimeMapping(DownloadFileType.APK)); + request.setMimeType("application/vnd.android.package-archive"); dlRequest = mDownloadManager.enqueue(request); diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestRunner.java index 27bf7e1e2497..27bf7e1e2497 100644 --- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestRunner.java diff --git a/core/tests/utillib/src/coretestutils/http/MockResponse.java b/core/tests/utillib/src/coretestutils/http/MockResponse.java deleted file mode 100644 index 5b03e5fe9019..000000000000 --- a/core/tests/utillib/src/coretestutils/http/MockResponse.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package coretestutils.http; - -import static coretestutils.http.MockWebServer.ASCII; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import android.util.Log; - -/** - * A scripted response to be replayed by the mock web server. - */ -public class MockResponse { - private static final byte[] EMPTY_BODY = new byte[0]; - static final String LOG_TAG = "coretestutils.http.MockResponse"; - - private String status = "HTTP/1.1 200 OK"; - private Map<String, String> headers = new HashMap<String, String>(); - private byte[] body = EMPTY_BODY; - private boolean closeConnectionAfter = false; - private String closeConnectionAfterHeader = null; - private int closeConnectionAfterXBytes = -1; - private int pauseConnectionAfterXBytes = -1; - private File bodyExternalFile = null; - - public MockResponse() { - addHeader("Content-Length", 0); - } - - /** - * Returns the HTTP response line, such as "HTTP/1.1 200 OK". - */ - public String getStatus() { - return status; - } - - public MockResponse setResponseCode(int code) { - this.status = "HTTP/1.1 " + code + " OK"; - return this; - } - - /** - * Returns the HTTP headers, such as "Content-Length: 0". - */ - public List<String> getHeaders() { - List<String> headerStrings = new ArrayList<String>(); - for (String header : headers.keySet()) { - headerStrings.add(header + ": " + headers.get(header)); - } - return headerStrings; - } - - public MockResponse addHeader(String header, String value) { - headers.put(header.toLowerCase(), value); - return this; - } - - public MockResponse addHeader(String header, long value) { - return addHeader(header, Long.toString(value)); - } - - public MockResponse removeHeader(String header) { - headers.remove(header.toLowerCase()); - return this; - } - - /** - * Returns true if the body should come from an external file, false otherwise. - */ - private boolean bodyIsExternal() { - return bodyExternalFile != null; - } - - /** - * Returns an input stream containing the raw HTTP payload. - */ - public InputStream getBody() { - if (bodyIsExternal()) { - try { - return new FileInputStream(bodyExternalFile); - } catch (FileNotFoundException e) { - Log.e(LOG_TAG, "File not found: " + bodyExternalFile.getAbsolutePath()); - } - } - return new ByteArrayInputStream(this.body); - } - - public MockResponse setBody(File body) { - addHeader("Content-Length", body.length()); - this.bodyExternalFile = body; - return this; - } - - public MockResponse setBody(byte[] body) { - addHeader("Content-Length", body.length); - this.body = body; - return this; - } - - public MockResponse setBody(String body) { - try { - return setBody(body.getBytes(ASCII)); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(); - } - } - - /** - * Sets the body as chunked. - * - * Currently chunked body is not supported for external files as bodies. - */ - public MockResponse setChunkedBody(byte[] body, int maxChunkSize) throws IOException { - addHeader("Transfer-encoding", "chunked"); - - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - int pos = 0; - while (pos < body.length) { - int chunkSize = Math.min(body.length - pos, maxChunkSize); - bytesOut.write(Integer.toHexString(chunkSize).getBytes(ASCII)); - bytesOut.write("\r\n".getBytes(ASCII)); - bytesOut.write(body, pos, chunkSize); - bytesOut.write("\r\n".getBytes(ASCII)); - pos += chunkSize; - } - bytesOut.write("0\r\n".getBytes(ASCII)); - this.body = bytesOut.toByteArray(); - return this; - } - - public MockResponse setChunkedBody(String body, int maxChunkSize) throws IOException { - return setChunkedBody(body.getBytes(ASCII), maxChunkSize); - } - - @Override public String toString() { - return status; - } - - public boolean shouldCloseConnectionAfter() { - return closeConnectionAfter; - } - - public MockResponse setCloseConnectionAfter(boolean closeConnectionAfter) { - this.closeConnectionAfter = closeConnectionAfter; - return this; - } - - /** - * Sets the header after which sending the server should close the connection. - */ - public MockResponse setCloseConnectionAfterHeader(String header) { - closeConnectionAfterHeader = header; - setCloseConnectionAfter(true); - return this; - } - - /** - * Returns the header after which sending the server should close the connection. - */ - public String getCloseConnectionAfterHeader() { - return closeConnectionAfterHeader; - } - - /** - * Sets the number of bytes in the body to send before which the server should close the - * connection. Set to -1 to unset and send the entire body (default). - */ - public MockResponse setCloseConnectionAfterXBytes(int position) { - closeConnectionAfterXBytes = position; - setCloseConnectionAfter(true); - return this; - } - - /** - * Returns the number of bytes in the body to send before which the server should close the - * connection. Returns -1 if the entire body should be sent (default). - */ - public int getCloseConnectionAfterXBytes() { - return closeConnectionAfterXBytes; - } - - /** - * Sets the number of bytes in the body to send before which the server should pause the - * connection (stalls in sending data). Only one pause per response is supported. - * Set to -1 to unset pausing (default). - */ - public MockResponse setPauseConnectionAfterXBytes(int position) { - pauseConnectionAfterXBytes = position; - return this; - } - - /** - * Returns the number of bytes in the body to send before which the server should pause the - * connection (stalls in sending data). (Returns -1 if it should not pause). - */ - public int getPauseConnectionAfterXBytes() { - return pauseConnectionAfterXBytes; - } - - /** - * Returns true if this response is flagged to pause the connection mid-stream, false otherwise - */ - public boolean getShouldPause() { - return (pauseConnectionAfterXBytes != -1); - } - - /** - * Returns true if this response is flagged to close the connection mid-stream, false otherwise - */ - public boolean getShouldClose() { - return (closeConnectionAfterXBytes != -1); - } -} diff --git a/core/tests/utillib/src/coretestutils/http/MockWebServer.java b/core/tests/utillib/src/coretestutils/http/MockWebServer.java deleted file mode 100644 index c329ffa518c1..000000000000 --- a/core/tests/utillib/src/coretestutils/http/MockWebServer.java +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package coretestutils.http; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.MalformedURLException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URL; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import android.util.Log; - -/** - * A scriptable web server. Callers supply canned responses and the server - * replays them upon request in sequence. - * - * TODO: merge with the version from libcore/support/src/tests/java once it's in. - */ -public final class MockWebServer { - static final String ASCII = "US-ASCII"; - static final String LOG_TAG = "coretestutils.http.MockWebServer"; - - private final BlockingQueue<RecordedRequest> requestQueue - = new LinkedBlockingQueue<RecordedRequest>(); - private final BlockingQueue<MockResponse> responseQueue - = new LinkedBlockingQueue<MockResponse>(); - private int bodyLimit = Integer.MAX_VALUE; - private final ExecutorService executor = Executors.newCachedThreadPool(); - // keep Futures around so we can rethrow any exceptions thrown by Callables - private final Queue<Future<?>> futures = new LinkedList<Future<?>>(); - private final Object downloadPauseLock = new Object(); - // global flag to signal when downloads should resume on the server - private volatile boolean downloadResume = false; - - private int port = -1; - - public int getPort() { - if (port == -1) { - throw new IllegalStateException("Cannot retrieve port before calling play()"); - } - return port; - } - - /** - * Returns a URL for connecting to this server. - * - * @param path the request path, such as "/". - */ - public URL getUrl(String path) throws MalformedURLException { - return new URL("http://localhost:" + getPort() + path); - } - - /** - * Sets the number of bytes of the POST body to keep in memory to the given - * limit. - */ - public void setBodyLimit(int maxBodyLength) { - this.bodyLimit = maxBodyLength; - } - - public void enqueue(MockResponse response) { - responseQueue.add(response); - } - - /** - * Awaits the next HTTP request, removes it, and returns it. Callers should - * use this to verify the request sent was as intended. - */ - public RecordedRequest takeRequest() throws InterruptedException { - return requestQueue.take(); - } - - public RecordedRequest takeRequestWithTimeout(long timeoutMillis) throws InterruptedException { - return requestQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS); - } - - public List<RecordedRequest> drainRequests() { - List<RecordedRequest> requests = new ArrayList<RecordedRequest>(); - requestQueue.drainTo(requests); - return requests; - } - - /** - * Starts the server, serves all enqueued requests, and shuts the server - * down using the default (server-assigned) port. - */ - public void play() throws IOException { - play(0); - } - - /** - * Starts the server, serves all enqueued requests, and shuts the server - * down. - * - * @param port The port number to use to listen to connections on; pass in 0 to have the - * server automatically assign a free port - */ - public void play(int portNumber) throws IOException { - final ServerSocket ss = new ServerSocket(portNumber); - ss.setReuseAddress(true); - port = ss.getLocalPort(); - submitCallable(new Callable<Void>() { - public Void call() throws Exception { - int count = 0; - while (true) { - if (count > 0 && responseQueue.isEmpty()) { - ss.close(); - executor.shutdown(); - return null; - } - - serveConnection(ss.accept()); - count++; - } - } - }); - } - - private void serveConnection(final Socket s) { - submitCallable(new Callable<Void>() { - public Void call() throws Exception { - InputStream in = new BufferedInputStream(s.getInputStream()); - OutputStream out = new BufferedOutputStream(s.getOutputStream()); - - int sequenceNumber = 0; - while (true) { - RecordedRequest request = readRequest(in, sequenceNumber); - if (request == null) { - if (sequenceNumber == 0) { - throw new IllegalStateException("Connection without any request!"); - } else { - break; - } - } - requestQueue.add(request); - MockResponse response = computeResponse(request); - writeResponse(out, response); - if (response.shouldCloseConnectionAfter()) { - break; - } - sequenceNumber++; - } - - in.close(); - out.close(); - return null; - } - }); - } - - private void submitCallable(Callable<?> callable) { - Future<?> future = executor.submit(callable); - futures.add(future); - } - - /** - * Check for and raise any exceptions that have been thrown by child threads. Will not block on - * children still running. - * @throws ExecutionException for the first child thread that threw an exception - */ - public void checkForExceptions() throws ExecutionException, InterruptedException { - final int originalSize = futures.size(); - for (int i = 0; i < originalSize; i++) { - Future<?> future = futures.remove(); - try { - future.get(0, TimeUnit.SECONDS); - } catch (TimeoutException e) { - futures.add(future); // still running - } - } - } - - /** - * @param sequenceNumber the index of this request on this connection. - */ - private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException { - String request = readAsciiUntilCrlf(in); - if (request.equals("")) { - return null; // end of data; no more requests - } - - List<String> headers = new ArrayList<String>(); - int contentLength = -1; - boolean chunked = false; - String header; - while (!(header = readAsciiUntilCrlf(in)).equals("")) { - headers.add(header); - String lowercaseHeader = header.toLowerCase(); - if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) { - contentLength = Integer.parseInt(header.substring(15).trim()); - } - if (lowercaseHeader.startsWith("transfer-encoding:") && - lowercaseHeader.substring(18).trim().equals("chunked")) { - chunked = true; - } - } - - boolean hasBody = false; - TruncatingOutputStream requestBody = new TruncatingOutputStream(); - List<Integer> chunkSizes = new ArrayList<Integer>(); - if (contentLength != -1) { - hasBody = true; - transfer(contentLength, in, requestBody); - } else if (chunked) { - hasBody = true; - while (true) { - int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16); - if (chunkSize == 0) { - readEmptyLine(in); - break; - } - chunkSizes.add(chunkSize); - transfer(chunkSize, in, requestBody); - readEmptyLine(in); - } - } - - if (request.startsWith("GET ")) { - if (hasBody) { - throw new IllegalArgumentException("GET requests should not have a body!"); - } - } else if (request.startsWith("POST ")) { - if (!hasBody) { - throw new IllegalArgumentException("POST requests must have a body!"); - } - } else { - throw new UnsupportedOperationException("Unexpected method: " + request); - } - - return new RecordedRequest(request, headers, chunkSizes, - requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber); - } - - /** - * Returns a response to satisfy {@code request}. - */ - private MockResponse computeResponse(RecordedRequest request) throws InterruptedException { - if (responseQueue.isEmpty()) { - throw new IllegalStateException("Unexpected request: " + request); - } - return responseQueue.take(); - } - - private void writeResponse(OutputStream out, MockResponse response) throws IOException { - out.write((response.getStatus() + "\r\n").getBytes(ASCII)); - boolean doCloseConnectionAfterHeader = (response.getCloseConnectionAfterHeader() != null); - - // Send headers - String closeConnectionAfterHeader = response.getCloseConnectionAfterHeader(); - for (String header : response.getHeaders()) { - out.write((header + "\r\n").getBytes(ASCII)); - - if (doCloseConnectionAfterHeader && header.startsWith(closeConnectionAfterHeader)) { - Log.i(LOG_TAG, "Closing connection after header" + header); - break; - } - } - - // Send actual body data - if (!doCloseConnectionAfterHeader) { - out.write(("\r\n").getBytes(ASCII)); - - InputStream body = response.getBody(); - final int READ_BLOCK_SIZE = 10000; // process blocks this size - byte[] currentBlock = new byte[READ_BLOCK_SIZE]; - int currentBlockSize = 0; - int writtenSoFar = 0; - - boolean shouldPause = response.getShouldPause(); - boolean shouldClose = response.getShouldClose(); - int pause = response.getPauseConnectionAfterXBytes(); - int close = response.getCloseConnectionAfterXBytes(); - - // Don't bother pausing if it's set to pause -after- the connection should be dropped - if (shouldPause && shouldClose && (pause > close)) { - shouldPause = false; - } - - // Process each block we read in... - while ((currentBlockSize = body.read(currentBlock)) != -1) { - int startIndex = 0; - int writeLength = currentBlockSize; - - // handle the case of pausing - if (shouldPause && (writtenSoFar + currentBlockSize >= pause)) { - writeLength = pause - writtenSoFar; - out.write(currentBlock, 0, writeLength); - out.flush(); - writtenSoFar += writeLength; - - // now pause... - try { - Log.i(LOG_TAG, "Pausing connection after " + pause + " bytes"); - // Wait until someone tells us to resume sending... - synchronized(downloadPauseLock) { - while (!downloadResume) { - downloadPauseLock.wait(); - } - // reset resume back to false - downloadResume = false; - } - } catch (InterruptedException e) { - Log.e(LOG_TAG, "Server was interrupted during pause in download."); - } - - startIndex = writeLength; - writeLength = currentBlockSize - writeLength; - } - - // handle the case of closing the connection - if (shouldClose && (writtenSoFar + writeLength > close)) { - writeLength = close - writtenSoFar; - out.write(currentBlock, startIndex, writeLength); - writtenSoFar += writeLength; - Log.i(LOG_TAG, "Closing connection after " + close + " bytes"); - break; - } - out.write(currentBlock, startIndex, writeLength); - writtenSoFar += writeLength; - } - } - out.flush(); - } - - /** - * Transfer bytes from {@code in} to {@code out} until either {@code length} - * bytes have been transferred or {@code in} is exhausted. - */ - private void transfer(int length, InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - while (length > 0) { - int count = in.read(buffer, 0, Math.min(buffer.length, length)); - if (count == -1) { - return; - } - out.write(buffer, 0, count); - length -= count; - } - } - - /** - * Returns the text from {@code in} until the next "\r\n", or null if - * {@code in} is exhausted. - */ - private String readAsciiUntilCrlf(InputStream in) throws IOException { - StringBuilder builder = new StringBuilder(); - while (true) { - int c = in.read(); - if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') { - builder.deleteCharAt(builder.length() - 1); - return builder.toString(); - } else if (c == -1) { - return builder.toString(); - } else { - builder.append((char) c); - } - } - } - - private void readEmptyLine(InputStream in) throws IOException { - String line = readAsciiUntilCrlf(in); - if (!line.equals("")) { - throw new IllegalStateException("Expected empty but was: " + line); - } - } - - /** - * An output stream that drops data after bodyLimit bytes. - */ - private class TruncatingOutputStream extends ByteArrayOutputStream { - private int numBytesReceived = 0; - @Override public void write(byte[] buffer, int offset, int len) { - numBytesReceived += len; - super.write(buffer, offset, Math.min(len, bodyLimit - count)); - } - @Override public void write(int oneByte) { - numBytesReceived++; - if (count < bodyLimit) { - super.write(oneByte); - } - } - } - - /** - * Trigger the server to resume sending the download - */ - public void doResumeDownload() { - synchronized (downloadPauseLock) { - downloadResume = true; - downloadPauseLock.notifyAll(); - } - } -} diff --git a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java b/core/tests/utillib/src/coretestutils/http/RecordedRequest.java deleted file mode 100644 index 293ff80dc6e9..000000000000 --- a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package coretestutils.http; - -import java.util.List; - -/** - * An HTTP request that came into the mock web server. - */ -public final class RecordedRequest { - private final String requestLine; - private final List<String> headers; - private final List<Integer> chunkSizes; - private final int bodySize; - private final byte[] body; - private final int sequenceNumber; - - RecordedRequest(String requestLine, List<String> headers, List<Integer> chunkSizes, - int bodySize, byte[] body, int sequenceNumber) { - this.requestLine = requestLine; - this.headers = headers; - this.chunkSizes = chunkSizes; - this.bodySize = bodySize; - this.body = body; - this.sequenceNumber = sequenceNumber; - } - - public String getRequestLine() { - return requestLine; - } - - public List<String> getHeaders() { - return headers; - } - - /** - * Returns the sizes of the chunks of this request's body, or an empty list - * if the request's body was empty or unchunked. - */ - public List<Integer> getChunkSizes() { - return chunkSizes; - } - - /** - * Returns the total size of the body of this POST request (before - * truncation). - */ - public int getBodySize() { - return bodySize; - } - - /** - * Returns the body of this POST request. This may be truncated. - */ - public byte[] getBody() { - return body; - } - - /** - * Returns the index of this request on its HTTP connection. Since a single - * HTTP connection may serve multiple requests, each request is assigned its - * own sequence number. - */ - public int getSequenceNumber() { - return sequenceNumber; - } - - @Override public String toString() { - return requestLine; - } - - public String getMethod() { - return getRequestLine().split(" ")[0]; - } - - public String getPath() { - return getRequestLine().split(" ")[1]; - } -} diff --git a/services/java/com/android/server/wm/AppWindowToken.java b/services/java/com/android/server/wm/AppWindowToken.java index c24c2d91d785..d1f1b44c69fd 100644 --- a/services/java/com/android/server/wm/AppWindowToken.java +++ b/services/java/com/android/server/wm/AppWindowToken.java @@ -393,8 +393,8 @@ class AppWindowToken extends WindowToken { if (!win.isDrawnLw()) { Slog.v(WindowManagerService.TAG, "Not displayed: s=" + win.mWinAnimator.mSurface + " pv=" + win.mPolicyVisibility - + " dp=" + win.mDrawPending - + " cdp=" + win.mCommitDrawPending + + " dp=" + win.mWinAnimator.mDrawPending + + " cdp=" + win.mWinAnimator.mCommitDrawPending + " ah=" + win.mAttachedHidden + " th=" + (win.mAppToken != null diff --git a/services/java/com/android/server/wm/WindowAnimator.java b/services/java/com/android/server/wm/WindowAnimator.java index eddfe9217d9a..26f4d0d663f0 100644 --- a/services/java/com/android/server/wm/WindowAnimator.java +++ b/services/java/com/android/server/wm/WindowAnimator.java @@ -4,6 +4,9 @@ package com.android.server.wm; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static com.android.server.wm.WindowManagerService.LayoutFields.SET_UPDATE_ROTATION; +import static com.android.server.wm.WindowManagerService.LayoutFields.SET_WALLPAPER_MAY_CHANGE; + import android.content.Context; import android.os.SystemClock; import android.util.Log; @@ -30,7 +33,6 @@ public class WindowAnimator { final WindowManagerPolicy mPolicy; boolean mAnimating; - boolean mUpdateRotation; boolean mTokenMayBeDrawn; boolean mForceHiding; WindowState mWindowAnimationBackground; @@ -61,9 +63,10 @@ public class WindowAnimator { // seen. WindowState mWindowDetachedWallpaper = null; WindowState mDetachedWallpaper = null; - boolean mWallpaperMayChange; DimSurface mWindowAnimationBackgroundSurface = null; + int mBulkUpdateParams = 0; + WindowAnimator(final WindowManagerService service, final Context context, final WindowManagerPolicy policy) { mService = service; @@ -77,7 +80,7 @@ public class WindowAnimator { "Detached wallpaper changed from " + mWindowDetachedWallpaper + " to " + mDetachedWallpaper); mWindowDetachedWallpaper = mDetachedWallpaper; - mWallpaperMayChange = true; + mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE; } if (mWindowAnimationBackgroundColor != 0) { @@ -147,10 +150,9 @@ public class WindowAnimator { (mScreenRotationAnimation.isAnimating() || mScreenRotationAnimation.mFinishAnimReady)) { if (mScreenRotationAnimation.stepAnimationLocked(mCurrentTime)) { - mUpdateRotation = false; mAnimating = true; } else { - mUpdateRotation = true; + mBulkUpdateParams |= SET_UPDATE_ROTATION; mScreenRotationAnimation.kill(); mScreenRotationAnimation = null; } @@ -217,7 +219,7 @@ public class WindowAnimator { } if (wasAnimating && !winAnimator.mAnimating && mService.mWallpaperTarget == w) { - mWallpaperMayChange = true; + mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE; mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; if (WindowManagerService.DEBUG_LAYOUT_REPEATS) { mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 2"); @@ -270,7 +272,7 @@ public class WindowAnimator { } if (changed && (attrs.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) { - mWallpaperMayChange = true; + mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE; mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; if (WindowManagerService.DEBUG_LAYOUT_REPEATS) { mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 4"); @@ -297,8 +299,8 @@ public class WindowAnimator { if (!w.isDrawnLw()) { Slog.v(TAG, "Not displayed: s=" + winAnimator.mSurface + " pv=" + w.mPolicyVisibility - + " dp=" + w.mDrawPending - + " cdp=" + w.mCommitDrawPending + + " dp=" + winAnimator.mDrawPending + + " cdp=" + winAnimator.mCommitDrawPending + " ah=" + w.mAttachedHidden + " th=" + atoken.hiddenRequested + " a=" + winAnimator.mAnimating); @@ -386,7 +388,6 @@ public class WindowAnimator { private void performAnimationsLocked() { mTokenMayBeDrawn = false; - mService.mInnerFields.mWallpaperMayChange = false; mForceHiding = false; mDetachedWallpaper = null; mWindowAnimationBackground = null; @@ -399,11 +400,10 @@ public class WindowAnimator { } } - void animate() { mPendingLayoutChanges = 0; - mWallpaperMayChange = false; mCurrentTime = SystemClock.uptimeMillis(); + mBulkUpdateParams = 0; // Update animations of all applications, including those // associated with exiting/removed apps @@ -445,8 +445,8 @@ public class WindowAnimator { Surface.closeTransaction(); } - if (mWallpaperMayChange) { - mService.notifyWallpaperMayChange(); + if (mBulkUpdateParams != 0) { + mService.bulkSetParameters(mBulkUpdateParams); } } diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index 3d60c6b2cf3a..a91e716d65aa 100644 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -589,7 +589,10 @@ public class WindowManagerService extends IWindowManager.Stub /** Pulled out of performLayoutAndPlaceSurfacesLockedInner in order to refactor into multiple * methods. */ - class LayoutAndSurfaceFields { + class LayoutFields { + static final int SET_UPDATE_ROTATION = 1 << 0; + static final int SET_WALLPAPER_MAY_CHANGE = 1 << 1; + boolean mWallpaperForceHidingChanged = false; boolean mWallpaperMayChange = false; boolean mOrientationChangeComplete = true; @@ -600,8 +603,9 @@ public class WindowManagerService extends IWindowManager.Stub private boolean mSyswin = false; private float mScreenBrightness = -1; private float mButtonBrightness = -1; + private boolean mUpdateRotation = false; } - LayoutAndSurfaceFields mInnerFields = new LayoutAndSurfaceFields(); + LayoutFields mInnerFields = new LayoutFields(); /** Only do a maximum of 6 repeated layouts. After that quit */ private int mLayoutRepeatCount; @@ -1547,6 +1551,7 @@ public class WindowManagerService extends IWindowManager.Stub static final int ADJUST_WALLPAPER_VISIBILITY_CHANGED = 1<<2; int adjustWallpaperWindowsLocked() { + mInnerFields.mWallpaperMayChange = false; int changed = 0; final int dw = mAppDisplayWidth; @@ -1584,8 +1589,8 @@ public class WindowManagerService extends IWindowManager.Stub } } if (DEBUG_WALLPAPER) Slog.v(TAG, "Win " + w + ": readyfordisplay=" - + w.isReadyForDisplay() + " drawpending=" + w.mDrawPending - + " commitdrawpending=" + w.mCommitDrawPending); + + w.isReadyForDisplay() + " drawpending=" + w.mWinAnimator.mDrawPending + + " commitdrawpending=" + w.mWinAnimator.mCommitDrawPending); if ((w.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0 && w.isReadyForDisplay() && (mWallpaperTarget == w || w.isDrawnLw())) { if (DEBUG_WALLPAPER) Slog.v(TAG, @@ -2944,7 +2949,7 @@ public class WindowManagerService extends IWindowManager.Stub final long origId = Binder.clearCallingIdentity(); synchronized(mWindowMap) { WindowState win = windowForClientLocked(session, client, false); - if (win != null && win.finishDrawingLocked()) { + if (win != null && win.mWinAnimator.finishDrawingLocked()) { if ((win.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0) { adjustWallpaperWindowsLocked(); } @@ -6651,6 +6656,7 @@ public class WindowManagerService extends IWindowManager.Stub public static final int REPORT_HARD_KEYBOARD_STATUS_CHANGE = 22; public static final int BOOT_TIMEOUT = 23; public static final int WAITING_FOR_DRAWN_TIMEOUT = 24; + public static final int BULK_UPDATE_PARAMETERS = 25; private Session mLastReportedHold; @@ -7061,6 +7067,21 @@ public class WindowManagerService extends IWindowManager.Stub } break; } + + case BULK_UPDATE_PARAMETERS: { + synchronized (mWindowMap) { + // TODO(cmautner): As the number of bits grows, use masks of bit groups to + // eliminate unnecessary tests. + if ((msg.arg1 & LayoutFields.SET_UPDATE_ROTATION) != 0) { + mInnerFields.mUpdateRotation = true; + } + if ((msg.arg1 & LayoutFields.SET_WALLPAPER_MAY_CHANGE) != 0) { + mInnerFields.mWallpaperMayChange = true; + } + + requestTraversalLocked(); + } + } } } } @@ -7995,7 +8016,6 @@ public class WindowManagerService extends IWindowManager.Stub } } mInnerFields.mAdjResult |= adjustWallpaperWindowsLocked(); - mInnerFields.mWallpaperMayChange = false; mInnerFields.mWallpaperForceHidingChanged = false; if (DEBUG_WALLPAPER) Slog.v(TAG, "****** OLD: " + oldWallpaper + " NEW: " + mWallpaperTarget @@ -8026,6 +8046,7 @@ public class WindowManagerService extends IWindowManager.Stub } private void updateResizingWindows(final WindowState w) { + final WindowStateAnimator winAnimator = w.mWinAnimator; if (!w.mAppFreezing && w.mLayoutSeq == mLayoutSeq) { w.mContentInsetsChanged |= !w.mLastContentInsets.equals(w.mContentInsets); @@ -8045,7 +8066,7 @@ public class WindowManagerService extends IWindowManager.Stub w.mLastFrame.set(w.mFrame); if (w.mContentInsetsChanged || w.mVisibleInsetsChanged - || w.mWinAnimator.mSurfaceResized + || winAnimator.mSurfaceResized || configChanged) { if (DEBUG_RESIZE || DEBUG_ORIENTATION) { Slog.v(TAG, "Resize reasons: " @@ -8067,8 +8088,8 @@ public class WindowManagerService extends IWindowManager.Stub if (DEBUG_ORIENTATION) Slog.v(TAG, "Orientation start waiting for draw in " + w + ", surface " + w.mWinAnimator.mSurface); - w.mDrawPending = true; - w.mCommitDrawPending = false; + winAnimator.mDrawPending = true; + winAnimator.mCommitDrawPending = false; w.mReadyToShow = false; if (w.mAppToken != null) { w.mAppToken.allDrawn = false; @@ -8377,7 +8398,7 @@ public class WindowManagerService extends IWindowManager.Stub // Moved from updateWindowsAndWallpaperLocked(). if (winAnimator.mSurface != null) { // Take care of the window being ready to display. - if (w.commitFinishDrawingLocked(currentTime)) { + if (winAnimator.commitFinishDrawingLocked(currentTime)) { if ((w.mAttrs.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) { if (WindowManagerService.DEBUG_WALLPAPER) Slog.v(TAG, @@ -8439,6 +8460,7 @@ public class WindowManagerService extends IWindowManager.Stub do { i--; WindowState win = mResizingWindows.get(i); + final WindowStateAnimator winAnimator = win.mWinAnimator; try { if (DEBUG_RESIZE || DEBUG_ORIENTATION) Slog.v(TAG, "Reporting new frame to " + win + ": " + win.mCompatFrame); @@ -8450,20 +8472,20 @@ public class WindowManagerService extends IWindowManager.Stub if ((DEBUG_RESIZE || DEBUG_ORIENTATION || DEBUG_CONFIGURATION) && configChanged) { Slog.i(TAG, "Sending new config to window " + win + ": " - + win.mWinAnimator.mSurfaceW + "x" + win.mWinAnimator.mSurfaceH + + winAnimator.mSurfaceW + "x" + winAnimator.mSurfaceH + " / " + mCurConfiguration + " / 0x" + Integer.toHexString(diff)); } win.mConfiguration = mCurConfiguration; - if (DEBUG_ORIENTATION && win.mDrawPending) Slog.i( + if (DEBUG_ORIENTATION && winAnimator.mDrawPending) Slog.i( TAG, "Resizing " + win + " WITH DRAW PENDING"); - win.mClient.resized((int)win.mWinAnimator.mSurfaceW, - (int)win.mWinAnimator.mSurfaceH, - win.mLastContentInsets, win.mLastVisibleInsets, win.mDrawPending, - configChanged ? win.mConfiguration : null); + win.mClient.resized((int)winAnimator.mSurfaceW, + (int)winAnimator.mSurfaceH, + win.mLastContentInsets, win.mLastVisibleInsets, + winAnimator.mDrawPending, configChanged ? win.mConfiguration : null); win.mContentInsetsChanged = false; win.mVisibleInsetsChanged = false; - win.mWinAnimator.mSurfaceResized = false; + winAnimator.mSurfaceResized = false; } catch (RemoteException e) { win.mOrientationChanging = false; } @@ -8574,17 +8596,17 @@ public class WindowManagerService extends IWindowManager.Stub mTurnOnScreen = false; } - if (mAnimator.mUpdateRotation) { + if (mInnerFields.mUpdateRotation) { if (DEBUG_ORIENTATION) Slog.d(TAG, "Performing post-rotate rotation"); if (updateRotationUncheckedLocked(false)) { mH.sendEmptyMessage(H.SEND_NEW_CONFIGURATION); } else { - mAnimator.mUpdateRotation = false; + mInnerFields.mUpdateRotation = false; } } if (mInnerFields.mOrientationChangeComplete && !mLayoutNeeded && - !mAnimator.mUpdateRotation) { + !mInnerFields.mUpdateRotation) { checkDrawnWindowsLocked(); } @@ -9646,15 +9668,14 @@ public class WindowManagerService extends IWindowManager.Stub requestTraversalLocked(); } - void notifyWallpaperMayChange() { - mInnerFields.mWallpaperMayChange = true; - requestTraversalLocked(); - } - void debugLayoutRepeats(final String msg) { if (mLayoutRepeatCount >= LAYOUT_REPEAT_THRESHOLD) { Slog.v(TAG, "Layouts looping: " + msg); Slog.v(TAG, "mPendingLayoutChanges = 0x" + Integer.toHexString(mPendingLayoutChanges)); } } + + void bulkSetParameters(final int bulkUpdateParams) { + mH.sendMessage(mH.obtainMessage(H.BULK_UPDATE_PARAMETERS, bulkUpdateParams, 0)); + } } diff --git a/services/java/com/android/server/wm/WindowState.java b/services/java/com/android/server/wm/WindowState.java index d22b17c1ac2a..7f6342962678 100644 --- a/services/java/com/android/server/wm/WindowState.java +++ b/services/java/com/android/server/wm/WindowState.java @@ -19,7 +19,6 @@ package com.android.server.wm; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW; import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; @@ -207,15 +206,6 @@ final class WindowState implements WindowManagerPolicy.WindowState { // when in that case until the layout is done. boolean mLayoutNeeded; - // This is set after the Surface has been created but before the - // window has been drawn. During this time the surface is hidden. - boolean mDrawPending; - - // This is set after the window has finished drawing for the first - // time but before its surface is shown. The surface will be - // displayed when the next layout is run. - boolean mCommitDrawPending; - // This is set during the time after the window's drawing has been // committed, and before its surface is actually shown. It is used // to delay showing the surface until all windows in a token are ready @@ -595,36 +585,6 @@ final class WindowState implements WindowManagerPolicy.WindowState { return mAppToken != null ? mAppToken.firstWindowDrawn : false; } - // TODO(cmautner): Move to WindowStateAnimator - boolean finishDrawingLocked() { - if (mDrawPending) { - if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION) Slog.v( - TAG, "finishDrawingLocked: " + this + " in " - + mWinAnimator.mSurface); - mCommitDrawPending = true; - mDrawPending = false; - return true; - } - return false; - } - - // TODO(cmautner): Move to WindowStateAnimator - // This must be called while inside a transaction. - boolean commitFinishDrawingLocked(long currentTime) { - //Slog.i(TAG, "commitFinishDrawingLocked: " + mSurface); - if (!mCommitDrawPending) { - return false; - } - mCommitDrawPending = false; - mReadyToShow = true; - final boolean starting = mAttrs.type == TYPE_APPLICATION_STARTING; - final AppWindowToken atoken = mAppToken; - if (atoken == null || atoken.allDrawn || starting) { - mWinAnimator.performShowLocked(); - } - return true; - } - boolean isIdentityMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { if (dsdx < .99999f || dsdx > 1.00001f) return false; if (dtdy < .99999f || dtdy > 1.00001f) return false; @@ -782,7 +742,7 @@ final class WindowState implements WindowManagerPolicy.WindowState { */ public boolean isDrawnLw() { return mWinAnimator.mSurface != null && !mDestroying - && !mDrawPending && !mCommitDrawPending; + && !mWinAnimator.mDrawPending && !mWinAnimator.mCommitDrawPending; } /** @@ -1087,8 +1047,8 @@ final class WindowState implements WindowManagerPolicy.WindowState { } mWinAnimator.dump(pw, prefix, dumpAll); if (dumpAll) { - pw.print(prefix); pw.print("mDrawPending="); pw.print(mDrawPending); - pw.print(" mCommitDrawPending="); pw.print(mCommitDrawPending); + pw.print(prefix); pw.print("mDrawPending="); pw.print(mWinAnimator.mDrawPending); + pw.print(" mCommitDrawPending="); pw.print(mWinAnimator.mCommitDrawPending); pw.print(" mReadyToShow="); pw.print(mReadyToShow); pw.print(" mHasDrawn="); pw.println(mHasDrawn); } diff --git a/services/java/com/android/server/wm/WindowStateAnimator.java b/services/java/com/android/server/wm/WindowStateAnimator.java index d1539ba7e002..e99340c998cb 100644 --- a/services/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/java/com/android/server/wm/WindowStateAnimator.java @@ -98,6 +98,15 @@ class WindowStateAnimator { // an enter animation. boolean mEnterAnimationPending; + // This is set after the Surface has been created but before the + // window has been drawn. During this time the surface is hidden. + boolean mDrawPending; + + // This is set after the window has finished drawing for the first + // time but before its surface is shown. The surface will be + // displayed when the next layout is run. + boolean mCommitDrawPending; + public WindowStateAnimator(final WindowManagerService service, final WindowState win, final WindowState attachedWindow) { mService = service; @@ -347,14 +356,41 @@ class WindowStateAnimator { } } + boolean finishDrawingLocked() { + if (mDrawPending) { + if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION) Slog.v( + TAG, "finishDrawingLocked: " + this + " in " + mSurface); + mCommitDrawPending = true; + mDrawPending = false; + return true; + } + return false; + } + + // This must be called while inside a transaction. + boolean commitFinishDrawingLocked(long currentTime) { + //Slog.i(TAG, "commitFinishDrawingLocked: " + mSurface); + if (!mCommitDrawPending) { + return false; + } + mCommitDrawPending = false; + mWin.mReadyToShow = true; + final boolean starting = mWin.mAttrs.type == TYPE_APPLICATION_STARTING; + final AppWindowToken atoken = mWin.mAppToken; + if (atoken == null || atoken.allDrawn || starting) { + performShowLocked(); + } + return true; + } + Surface createSurfaceLocked() { if (mSurface == null) { mReportDestroySurface = false; mSurfacePendingDestroy = false; if (WindowManagerService.DEBUG_ORIENTATION) Slog.i(TAG, "createSurface " + this + ": DRAW NOW PENDING"); - mWin.mDrawPending = true; - mWin.mCommitDrawPending = false; + mDrawPending = true; + mCommitDrawPending = false; mWin.mReadyToShow = false; if (mWin.mAppToken != null) { mWin.mAppToken.allDrawn = false; @@ -471,8 +507,8 @@ class WindowStateAnimator { } if (mSurface != null) { - mWin.mDrawPending = false; - mWin.mCommitDrawPending = false; + mDrawPending = false; + mCommitDrawPending = false; mWin.mReadyToShow = false; int i = mWin.mChildWindows.size(); diff --git a/voip/java/android/net/rtp/AudioGroup.java b/voip/java/android/net/rtp/AudioGroup.java index 3e7ace83c99b..8c190621934f 100644 --- a/voip/java/android/net/rtp/AudioGroup.java +++ b/voip/java/android/net/rtp/AudioGroup.java @@ -142,34 +142,34 @@ public class AudioGroup { private native void nativeSetMode(int mode); // Package-private method used by AudioStream.join(). - synchronized void add(AudioStream stream, AudioCodec codec, int dtmfType) { + synchronized void add(AudioStream stream) { if (!mStreams.containsKey(stream)) { try { - int socket = stream.dup(); + AudioCodec codec = stream.getCodec(); String codecSpec = String.format("%d %s %s", codec.type, codec.rtpmap, codec.fmtp); - nativeAdd(stream.getMode(), socket, + int id = nativeAdd(stream.getMode(), stream.getSocket(), stream.getRemoteAddress().getHostAddress(), - stream.getRemotePort(), codecSpec, dtmfType); - mStreams.put(stream, socket); + stream.getRemotePort(), codecSpec, stream.getDtmfType()); + mStreams.put(stream, id); } catch (NullPointerException e) { throw new IllegalStateException(e); } } } - private native void nativeAdd(int mode, int socket, String remoteAddress, + private native int nativeAdd(int mode, int socket, String remoteAddress, int remotePort, String codecSpec, int dtmfType); // Package-private method used by AudioStream.join(). synchronized void remove(AudioStream stream) { - Integer socket = mStreams.remove(stream); - if (socket != null) { - nativeRemove(socket); + Integer id = mStreams.remove(stream); + if (id != null) { + nativeRemove(id); } } - private native void nativeRemove(int socket); + private native void nativeRemove(int id); /** * Sends a DTMF digit to every {@link AudioStream} in this group. Currently @@ -192,15 +192,14 @@ public class AudioGroup { * Removes every {@link AudioStream} in this group. */ public void clear() { - synchronized (this) { - mStreams.clear(); - nativeRemove(-1); + for (AudioStream stream : getStreams()) { + stream.join(null); } } @Override protected void finalize() throws Throwable { - clear(); + nativeRemove(0); super.finalize(); } } diff --git a/voip/java/android/net/rtp/AudioStream.java b/voip/java/android/net/rtp/AudioStream.java index b7874f753fde..5cd1abcb0376 100644 --- a/voip/java/android/net/rtp/AudioStream.java +++ b/voip/java/android/net/rtp/AudioStream.java @@ -94,7 +94,7 @@ public class AudioStream extends RtpStream { mGroup = null; } if (group != null) { - group.add(this, mCodec, mDtmfType); + group.add(this); mGroup = group; } } diff --git a/voip/java/android/net/rtp/RtpStream.java b/voip/java/android/net/rtp/RtpStream.java index e94ac426430b..b9d75cd07824 100644 --- a/voip/java/android/net/rtp/RtpStream.java +++ b/voip/java/android/net/rtp/RtpStream.java @@ -54,7 +54,7 @@ public class RtpStream { private int mRemotePort = -1; private int mMode = MODE_NORMAL; - private int mNative; + private int mSocket = -1; static { System.loadLibrary("rtp_jni"); } @@ -165,7 +165,9 @@ public class RtpStream { mRemotePort = port; } - synchronized native int dup(); + int getSocket() { + return mSocket; + } /** * Releases allocated resources. The stream becomes inoperable after calling @@ -175,13 +177,15 @@ public class RtpStream { * @see #isBusy() */ public void release() { - if (isBusy()) { - throw new IllegalStateException("Busy"); + synchronized (this) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + close(); } - close(); } - private synchronized native void close(); + private native void close(); @Override protected void finalize() throws Throwable { diff --git a/voip/jni/rtp/AudioGroup.cpp b/voip/jni/rtp/AudioGroup.cpp index b9bbd164ded7..673a6504300a 100644 --- a/voip/jni/rtp/AudioGroup.cpp +++ b/voip/jni/rtp/AudioGroup.cpp @@ -478,7 +478,7 @@ public: bool setMode(int mode); bool sendDtmf(int event); bool add(AudioStream *stream); - bool remove(int socket); + bool remove(AudioStream *stream); bool platformHasAec() { return mPlatformHasAec; } private: @@ -691,20 +691,19 @@ bool AudioGroup::add(AudioStream *stream) return true; } -bool AudioGroup::remove(int socket) +bool AudioGroup::remove(AudioStream *stream) { mNetworkThread->requestExitAndWait(); - for (AudioStream *stream = mChain; stream->mNext; stream = stream->mNext) { - AudioStream *target = stream->mNext; - if (target->mSocket == socket) { - if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, socket, NULL)) { + for (AudioStream *chain = mChain; chain->mNext; chain = chain->mNext) { + if (chain->mNext == stream) { + if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, stream->mSocket, NULL)) { ALOGE("epoll_ctl: %s", strerror(errno)); return false; } - stream->mNext = target->mNext; - ALOGD("stream[%d] leaves group[%d]", socket, mDeviceSocket); - delete target; + chain->mNext = stream->mNext; + ALOGD("stream[%d] leaves group[%d]", stream->mSocket, mDeviceSocket); + delete stream; break; } } @@ -931,7 +930,7 @@ exit: static jfieldID gNative; static jfieldID gMode; -void add(JNIEnv *env, jobject thiz, jint mode, +int add(JNIEnv *env, jobject thiz, jint mode, jint socket, jstring jRemoteAddress, jint remotePort, jstring jCodecSpec, jint dtmfType) { @@ -943,16 +942,22 @@ void add(JNIEnv *env, jobject thiz, jint mode, sockaddr_storage remote; if (parse(env, jRemoteAddress, remotePort, &remote) < 0) { // Exception already thrown. - return; + return 0; } if (!jCodecSpec) { jniThrowNullPointerException(env, "codecSpec"); - return; + return 0; } const char *codecSpec = env->GetStringUTFChars(jCodecSpec, NULL); if (!codecSpec) { // Exception already thrown. - return; + return 0; + } + socket = dup(socket); + if (socket == -1) { + jniThrowException(env, "java/lang/IllegalStateException", + "cannot get stream socket"); + return 0; } // Create audio codec. @@ -1001,7 +1006,7 @@ void add(JNIEnv *env, jobject thiz, jint mode, // Succeed. env->SetIntField(thiz, gNative, (int)group); - return; + return (int)stream; error: delete group; @@ -1009,13 +1014,14 @@ error: delete codec; close(socket); env->SetIntField(thiz, gNative, 0); + return 0; } -void remove(JNIEnv *env, jobject thiz, jint socket) +void remove(JNIEnv *env, jobject thiz, jint stream) { AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative); if (group) { - if (socket == -1 || !group->remove(socket)) { + if (!stream || !group->remove((AudioStream *)stream)) { delete group; env->SetIntField(thiz, gNative, 0); } @@ -1039,7 +1045,7 @@ void sendDtmf(JNIEnv *env, jobject thiz, jint event) } JNINativeMethod gMethods[] = { - {"nativeAdd", "(IILjava/lang/String;ILjava/lang/String;I)V", (void *)add}, + {"nativeAdd", "(IILjava/lang/String;ILjava/lang/String;I)I", (void *)add}, {"nativeRemove", "(I)V", (void *)remove}, {"nativeSetMode", "(I)V", (void *)setMode}, {"nativeSendDtmf", "(I)V", (void *)sendDtmf}, diff --git a/voip/jni/rtp/RtpStream.cpp b/voip/jni/rtp/RtpStream.cpp index 6540099e83f2..bfe8e24b4fd3 100644 --- a/voip/jni/rtp/RtpStream.cpp +++ b/voip/jni/rtp/RtpStream.cpp @@ -33,11 +33,11 @@ extern int parse(JNIEnv *env, jstring jAddress, int port, sockaddr_storage *ss); namespace { -jfieldID gNative; +jfieldID gSocket; jint create(JNIEnv *env, jobject thiz, jstring jAddress) { - env->SetIntField(thiz, gNative, -1); + env->SetIntField(thiz, gSocket, -1); sockaddr_storage ss; if (parse(env, jAddress, 0, &ss) < 0) { @@ -58,7 +58,7 @@ jint create(JNIEnv *env, jobject thiz, jstring jAddress) &((sockaddr_in *)&ss)->sin_port : &((sockaddr_in6 *)&ss)->sin6_port; uint16_t port = ntohs(*p); if ((port & 1) == 0) { - env->SetIntField(thiz, gNative, socket); + env->SetIntField(thiz, gSocket, socket); return port; } ::close(socket); @@ -75,7 +75,7 @@ jint create(JNIEnv *env, jobject thiz, jstring jAddress) *p = htons(port); if (bind(socket, (sockaddr *)&ss, sizeof(ss)) == 0) { - env->SetIntField(thiz, gNative, socket); + env->SetIntField(thiz, gSocket, socket); return port; } } @@ -86,25 +86,15 @@ jint create(JNIEnv *env, jobject thiz, jstring jAddress) return -1; } -jint dup(JNIEnv *env, jobject thiz) -{ - int socket = ::dup(env->GetIntField(thiz, gNative)); - if (socket == -1) { - jniThrowException(env, "java/lang/IllegalStateException", strerror(errno)); - } - return socket; -} - void close(JNIEnv *env, jobject thiz) { - int socket = env->GetIntField(thiz, gNative); + int socket = env->GetIntField(thiz, gSocket); ::close(socket); - env->SetIntField(thiz, gNative, -1); + env->SetIntField(thiz, gSocket, -1); } JNINativeMethod gMethods[] = { {"create", "(Ljava/lang/String;)I", (void *)create}, - {"dup", "()I", (void *)dup}, {"close", "()V", (void *)close}, }; @@ -114,7 +104,7 @@ int registerRtpStream(JNIEnv *env) { jclass clazz; if ((clazz = env->FindClass("android/net/rtp/RtpStream")) == NULL || - (gNative = env->GetFieldID(clazz, "mNative", "I")) == NULL || + (gSocket = env->GetFieldID(clazz, "mSocket", "I")) == NULL || env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) { ALOGE("JNI registration failed"); return -1; diff --git a/wifi/java/android/net/wifi/WifiConfigStore.java b/wifi/java/android/net/wifi/WifiConfigStore.java index a9dbd10b293e..3c761c802439 100644 --- a/wifi/java/android/net/wifi/WifiConfigStore.java +++ b/wifi/java/android/net/wifi/WifiConfigStore.java @@ -1141,7 +1141,15 @@ class WifiConfigStore { String varName = field.varName(); String value = field.value(); if (value != null) { - if (field != config.eap && field != config.engine) { + if (field == config.engine) { + /* + * If the field is declared as an integer, it must not + * be null + */ + if (value.length() == 0) { + value = "0"; + } + } else if (field != config.eap) { value = (value.length() == 0) ? "NULL" : convertToQuotedString(value); } if (!mWifiNative.setNetworkVariable( |