Merge "java-side enable/disable switch for link prefetch via WebSettings"
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index b708750..d0c87c6 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -338,7 +338,7 @@
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 @@
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 @@
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 0000000..880dc34
--- /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 4bdb3e2..2a81f08 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,11 +18,8 @@
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.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.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.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.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 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 @@
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 @@
mDrawableHeightStart, mDrawableHeightEnd;
int mDrawablePadding;
}
- private Drawables mDrawables;
+ Drawables mDrawables;
private CharWrapper mCharWrapper;
@@ -404,23 +379,23 @@
// 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 @@
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 @@
setClickable(clickable);
setLongClickable(longClickable);
- prepareCursorControllers();
+ if (mEditor != null) mEditor.prepareCursorControllers();
}
private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
@@ -1216,11 +1185,13 @@
}
// 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 @@
fixFocusableAndClickableSettings();
// SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
- prepareCursorControllers();
+ if (mEditor != null) getEditor().prepareCursorControllers();
}
}
@@ -3250,7 +3221,7 @@
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
- prepareCursorControllers();
+ if (mEditor != null) getEditor().prepareCursorControllers();
}
/**
@@ -3366,12 +3337,37 @@
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 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 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 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 @@
* @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 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 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 @@
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 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();
- }
- }
+ getEditor().setError(error, icon);
}
- 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());
- }
-
-
@Override
protected boolean setFrame(int l, int t, int r, int b) {
boolean result = super.setFrame(l, t, r, b);
@@ -4009,7 +3864,7 @@
/////////////////////////////////////////////////////////////////////////
- private int getVerticalOffset(boolean forceNormal) {
+ int getVerticalOffset(boolean forceNormal) {
int voffset = 0;
final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
@@ -4071,7 +3926,7 @@
return voffset;
}
- private void invalidateCursorPath() {
+ void invalidateCursorPath() {
if (mHighlightPathBogus) {
invalidateCursor();
} else {
@@ -4114,7 +3969,7 @@
}
}
- private void invalidateCursor() {
+ void invalidateCursor() {
int where = getSelectionEnd();
invalidateCursor(where, where, where);
@@ -4130,8 +3985,6 @@
/**
* 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 @@
// - 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 @@
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 @@
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 @@
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 @@
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 @@
/**
* @hide
- * @param offsetRequired
*/
@Override
protected int getFadeTop(boolean offsetRequired) {
@@ -4871,7 +4719,6 @@
/**
* @hide
- * @param offsetRequired
*/
@Override
protected int getFadeHeight(boolean offsetRequired) {
@@ -5252,9 +5099,7 @@
@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 @@
* 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 @@
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 @@
// 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 @@
* @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 @@
mBoring = mHintBoring = null;
// Since it depends on the value of mLayout
- prepareCursorControllers();
+ if (mEditor != null) getEditor().prepareCursorControllers();
}
/**
@@ -5826,7 +5488,7 @@
}
// 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 @@
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 @@
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 @@
}
}
+ 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 @@
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 @@
if (oldStart >= 0 || newStart >= 0) {
invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
registerForPreDraw();
- makeBlink();
+ if (mEditor != null) getEditor().makeBlink();
}
}
@@ -7186,20 +6860,6 @@
}
/**
- * 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 @@
// 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 @@
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (mEditor != null && visibility != VISIBLE) {
- hideControllers();
+ getEditor().hideControllers();
}
}
@@ -7350,31 +7010,8 @@
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 @@
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 @@
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 @@
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 @@
/**
* 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 @@
*
* 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 @@
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 @@
}
/**
+ * 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());
+ if (getEditor() != null) {
+ return mEditor.getWordIterator();
+ } else {
+ return null;
}
- 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);
- }
- 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 @@
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 @@
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 @@
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 @@
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 @@
}
/**
- *
- * @return true if the selection mode was actually started.
- */
- private boolean startSelectionActionMode() {
- if (getEditor().mSelectionActionMode != null) {
- // Selection action mode is already started
- return false;
- }
-
- if (!canSelectText() || !requestFocus()) {
- Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
- 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;
- }
- }
-
- 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();
- getEditor().mSelectionActionMode = startActionMode(actionModeCallback);
- }
-
- 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);
- }
- }
-
- return selectionStarted;
- }
-
- private boolean extractedTextModeWillBeStarted() {
- if (!(this instanceof ExtractEditText)) {
- final InputMethodManager imm = InputMethodManager.peekInstance();
- return imm != null && imm.isFullscreenMode();
- }
- return false;
- }
-
- /**
* @hide
*/
protected void stopSelectionActionMode() {
- if (getEditor().mSelectionActionMode != null) {
- // This will hide the mSelectionModifierCursorController
- getEditor().mSelectionActionMode.finish();
+ getEditor().stopSelectionActionMode();
+ }
+
+ boolean canCut() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
}
+
+ if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean canCopy() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
+ }
+
+ if (mText.length() > 0 && hasSelection()) {
+ return true;
+ }
+
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
}
}
- 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 @@
}
}
- /**
- * 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 @@
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 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 @@
+ " 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 5a7d519..93f90f6 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -438,10 +438,9 @@
* 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 @@
*/
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 6a471ad..b2075ae 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.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.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 @@
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 @@
* 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 @@
assertEquals(1, cursor.getCount());
assertTrue(cursor.moveToFirst());
- mServer.checkForExceptions();
-
verifyFileSize(pfd, fileSize);
verifyFileContents(pfd, fileData);
} finally {
@@ -928,7 +922,7 @@
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 @@
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 @@
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 afe7f55..aa9f69d 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.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 void setUp() throws Exception {
super.setUp();
setWiFiStateOn(true);
- mServer.play();
removeAllCurrentDownloads();
}
@@ -132,8 +131,6 @@
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 @@
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 @@
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 @@
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 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 @@
// 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 @@
@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 @@
@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 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 @@
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 @@
}
/**
- * 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 bdeb554..864b2d6 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
@@ -46,7 +46,6 @@
public void setUp() throws Exception {
super.setUp();
setWiFiStateOn(true);
- mServer.play();
removeAllCurrentDownloads();
}
@@ -85,7 +84,7 @@
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 @@
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 a419068..09dcac5 100644
--- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
+++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
@@ -20,7 +20,7 @@
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
similarity index 73%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java
index 334661d..8e935f8 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 @@
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.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 @@
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 @@
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 @@
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 @@
/**
* 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 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 @@
}
/**
- * 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 @@
}
/**
- * 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
similarity index 97%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java
index 654f747..9c44d61 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.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 @@
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
similarity index 100%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java
rename to 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 5b03e5f..0000000
--- 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 c329ffa..0000000
--- 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 293ff80..0000000
--- 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 c24c2d9..d1f1b44 100644
--- a/services/java/com/android/server/wm/AppWindowToken.java
+++ b/services/java/com/android/server/wm/AppWindowToken.java
@@ -393,8 +393,8 @@
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 eddfe92..26f4d0d 100644
--- a/services/java/com/android/server/wm/WindowAnimator.java
+++ b/services/java/com/android/server/wm/WindowAnimator.java
@@ -4,6 +4,9 @@
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 @@
final WindowManagerPolicy mPolicy;
boolean mAnimating;
- boolean mUpdateRotation;
boolean mTokenMayBeDrawn;
boolean mForceHiding;
WindowState mWindowAnimationBackground;
@@ -61,9 +63,10 @@
// 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 @@
"Detached wallpaper changed from " + mWindowDetachedWallpaper
+ " to " + mDetachedWallpaper);
mWindowDetachedWallpaper = mDetachedWallpaper;
- mWallpaperMayChange = true;
+ mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
}
if (mWindowAnimationBackgroundColor != 0) {
@@ -147,10 +150,9 @@
(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 @@
}
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 @@
}
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 @@
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 @@
private void performAnimationsLocked() {
mTokenMayBeDrawn = false;
- mService.mInnerFields.mWallpaperMayChange = false;
mForceHiding = false;
mDetachedWallpaper = null;
mWindowAnimationBackground = null;
@@ -399,11 +400,10 @@
}
}
-
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 @@
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 3d60c6b..a91e716 100644
--- a/services/java/com/android/server/wm/WindowManagerService.java
+++ b/services/java/com/android/server/wm/WindowManagerService.java
@@ -589,7 +589,10 @@
/** 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 @@
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 @@
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 @@
}
}
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 @@
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 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 @@
}
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 @@
}
}
mInnerFields.mAdjResult |= adjustWallpaperWindowsLocked();
- mInnerFields.mWallpaperMayChange = false;
mInnerFields.mWallpaperForceHidingChanged = false;
if (DEBUG_WALLPAPER) Slog.v(TAG, "****** OLD: " + oldWallpaper
+ " NEW: " + mWallpaperTarget
@@ -8026,6 +8046,7 @@
}
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 d22b17c..7f63429 100644
--- a/services/java/com/android/server/wm/WindowState.java
+++ b/services/java/com/android/server/wm/WindowState.java
@@ -19,7 +19,6 @@
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 @@
// 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 @@
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 @@
*/
public boolean isDrawnLw() {
return mWinAnimator.mSurface != null && !mDestroying
- && !mDrawPending && !mCommitDrawPending;
+ && !mWinAnimator.mDrawPending && !mWinAnimator.mCommitDrawPending;
}
/**
@@ -1087,8 +1047,8 @@
}
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 d1539ba..e99340c 100644
--- a/services/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/java/com/android/server/wm/WindowStateAnimator.java
@@ -98,6 +98,15 @@
// 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 @@
}
}
+ 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 @@
}
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 3e7ace8..8c19062 100644
--- a/voip/java/android/net/rtp/AudioGroup.java
+++ b/voip/java/android/net/rtp/AudioGroup.java
@@ -142,34 +142,34 @@
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 @@
* 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 b7874f7..5cd1abc 100644
--- a/voip/java/android/net/rtp/AudioStream.java
+++ b/voip/java/android/net/rtp/AudioStream.java
@@ -94,7 +94,7 @@
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 e94ac42..b9d75cd 100644
--- a/voip/java/android/net/rtp/RtpStream.java
+++ b/voip/java/android/net/rtp/RtpStream.java
@@ -54,7 +54,7 @@
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 @@
mRemotePort = port;
}
- synchronized native int dup();
+ int getSocket() {
+ return mSocket;
+ }
/**
* Releases allocated resources. The stream becomes inoperable after calling
@@ -175,13 +177,15 @@
* @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 b9bbd16..673a650 100644
--- a/voip/jni/rtp/AudioGroup.cpp
+++ b/voip/jni/rtp/AudioGroup.cpp
@@ -478,7 +478,7 @@
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 @@
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 @@
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 @@
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 @@
// Succeed.
env->SetIntField(thiz, gNative, (int)group);
- return;
+ return (int)stream;
error:
delete group;
@@ -1009,13 +1014,14 @@
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 @@
}
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 6540099..bfe8e24 100644
--- a/voip/jni/rtp/RtpStream.cpp
+++ b/voip/jni/rtp/RtpStream.cpp
@@ -33,11 +33,11 @@
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 @@
&((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 @@
*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 @@
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 @@
{
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 a9dbd10..3c761c8 100644
--- a/wifi/java/android/net/wifi/WifiConfigStore.java
+++ b/wifi/java/android/net/wifi/WifiConfigStore.java
@@ -1141,7 +1141,15 @@
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(