diff options
Diffstat (limited to 'core/java')
33 files changed, 5177 insertions, 4238 deletions
diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java index 24d3a6b..cff16ff 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -611,6 +611,10 @@ public abstract class ActionBar { * If the window hosting the ActionBar does not have the feature * {@link Window#FEATURE_ACTION_BAR_OVERLAY} it will resize application * content to fit the new space available. + * + * <p>If you are hiding the ActionBar through + * {@link View#SYSTEM_UI_FLAG_FULLSCREEN View.SYSTEM_UI_FLAG_FULLSCREEN}, + * you should not call this function directly. */ public abstract void show(); @@ -619,6 +623,12 @@ public abstract class ActionBar { * If the window hosting the ActionBar does not have the feature * {@link Window#FEATURE_ACTION_BAR_OVERLAY} it will resize application * content to fit the new space available. + * + * <p>Instead of calling this function directly, you can also cause an + * ActionBar using the overlay feature to hide through + * {@link View#SYSTEM_UI_FLAG_FULLSCREEN View.SYSTEM_UI_FLAG_FULLSCREEN}. + * Hiding the ActionBar through this system UI flag allows you to more + * seamlessly hide it in conjunction with other screen decorations. */ public abstract void hide(); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index ab4e73d..98c4e10 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -174,8 +174,10 @@ public final class ActivityThread { static final ThreadLocal<ActivityThread> sThreadLocal = new ThreadLocal<ActivityThread>(); Instrumentation mInstrumentation; String mInstrumentationAppDir = null; + String mInstrumentationAppLibraryDir = null; String mInstrumentationAppPackage = null; String mInstrumentedAppDir = null; + String mInstrumentedAppLibraryDir = null; boolean mSystemThread = false; boolean mJitEnabled = false; @@ -3936,8 +3938,10 @@ public final class ActivityThread { } mInstrumentationAppDir = ii.sourceDir; + mInstrumentationAppLibraryDir = ii.nativeLibraryDir; mInstrumentationAppPackage = ii.packageName; mInstrumentedAppDir = data.info.getAppDir(); + mInstrumentedAppLibraryDir = data.info.getLibDir(); ApplicationInfo instrApp = new ApplicationInfo(); instrApp.packageName = ii.packageName; diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 5340fbb..8ab1ed6 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -261,6 +261,7 @@ public final class LoadedApk { if (mIncludeCode && !mPackageName.equals("android")) { String zip = mAppDir; + String libraryPath = mLibDir; /* * The following is a bit of a hack to inject @@ -273,15 +274,20 @@ public final class LoadedApk { String instrumentationAppDir = mActivityThread.mInstrumentationAppDir; + String instrumentationAppLibraryDir = + mActivityThread.mInstrumentationAppLibraryDir; String instrumentationAppPackage = mActivityThread.mInstrumentationAppPackage; String instrumentedAppDir = mActivityThread.mInstrumentedAppDir; + String instrumentedAppLibraryDir = + mActivityThread.mInstrumentedAppLibraryDir; String[] instrumentationLibs = null; if (mAppDir.equals(instrumentationAppDir) || mAppDir.equals(instrumentedAppDir)) { zip = instrumentationAppDir + ":" + instrumentedAppDir; + libraryPath = instrumentationAppLibraryDir + ":" + instrumentedAppLibraryDir; if (! instrumentedAppDir.equals(instrumentationAppDir)) { instrumentationLibs = getLibrariesFor(instrumentationAppPackage); @@ -301,7 +307,7 @@ public final class LoadedApk { */ if (ActivityThread.localLOGV) - Slog.v(ActivityThread.TAG, "Class path: " + zip + ", JNI path: " + mLibDir); + Slog.v(ActivityThread.TAG, "Class path: " + zip + ", JNI path: " + libraryPath); // Temporarily disable logging of disk reads on the Looper thread // as this is early and necessary. @@ -309,7 +315,7 @@ public final class LoadedApk { mClassLoader = ApplicationLoaders.getDefault().getClassLoader( - zip, mLibDir, mBaseClassLoader); + zip, libraryPath, mBaseClassLoader); initializeJavaContextClassLoader(); StrictMode.setThreadPolicy(oldPolicy); @@ -442,6 +448,10 @@ public final class LoadedApk { return mAppDir; } + public String getLibDir() { + return mLibDir; + } + public String getResDir() { return mResDir; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 1356801..096af93 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -28,6 +28,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.IntProperty; +import android.util.Log; import android.view.View; import android.widget.ProgressBar; import android.widget.RemoteViews; @@ -808,7 +809,7 @@ public class Notification implements Parcelable public void setLatestEventInfo(Context context, CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent) { RemoteViews contentView = new RemoteViews(context.getPackageName(), - R.layout.status_bar_latest_event_content); + R.layout.notification_template_base); if (this.icon != 0) { contentView.setImageViewResource(R.id.icon, this.icon); } @@ -1439,11 +1440,28 @@ public class Notification implements Parcelable return contentView; } + private RemoteViews applyStandardTemplateWithActions(int layoutId) { + RemoteViews big = applyStandardTemplate(layoutId); + + int N = mActions.size(); + if (N > 0) { + Log.d("Notification", "has actions: " + mContentText); + big.setViewVisibility(R.id.actions, View.VISIBLE); + if (N>3) N=3; + for (int i=0; i<N; i++) { + final RemoteViews button = generateActionButton(mActions.get(i)); + Log.d("Notification", "adding action " + i + ": " + mActions.get(i).title); + big.addView(R.id.actions, button); + } + } + return big; + } + private RemoteViews makeContentView() { if (mContentView != null) { return mContentView; } else { - return applyStandardTemplate(R.layout.status_bar_latest_event_content); // no more special large_icon flavor + return applyStandardTemplate(R.layout.notification_template_base); // no more special large_icon flavor } } @@ -1461,6 +1479,12 @@ public class Notification implements Parcelable } } + private RemoteViews makeBigContentView() { + if (mActions.size() == 0) return null; + + return applyStandardTemplateWithActions(R.layout.notification_template_base); + } + private RemoteViews makeIntruderView(boolean showLabels) { RemoteViews intruderView = new RemoteViews(mContext.getPackageName(), R.layout.notification_intruder_content); @@ -1500,6 +1524,15 @@ public class Notification implements Parcelable return intruderView; } + private RemoteViews generateActionButton(Action action) { + RemoteViews button = new RemoteViews(mContext.getPackageName(), R.layout.notification_action); + button.setTextViewCompoundDrawables(R.id.action0, action.icon, 0, 0, 0); + button.setTextViewText(R.id.action0, action.title); + button.setOnClickPendingIntent(R.id.action0, action.actionIntent); + button.setContentDescription(R.id.action0, action.title); + return button; + } + /** * Combine all of the options that have been set and return a new {@link Notification} * object. @@ -1528,6 +1561,7 @@ public class Notification implements Parcelable if (mCanHasIntruder) { n.intruderView = makeIntruderView(mIntruderActionsShowText); } + n.bigContentView = makeBigContentView(); if (mLedOnMs != 0 && mLedOffMs != 0) { n.flags |= FLAG_SHOW_LIGHTS; } @@ -1583,7 +1617,7 @@ public class Notification implements Parcelable } private RemoteViews makeBigContentView() { - RemoteViews contentView = mBuilder.applyStandardTemplate(R.layout.notification_template_big_picture); + RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(R.layout.notification_template_big_picture); contentView.setImageViewBitmap(R.id.big_picture, mPicture); @@ -1630,7 +1664,7 @@ public class Notification implements Parcelable } private RemoteViews makeBigContentView() { - RemoteViews contentView = mBuilder.applyStandardTemplate(R.layout.status_bar_latest_event_content); + RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(R.layout.notification_template_base); contentView.setTextViewText(R.id.big_text, mBigText); contentView.setViewVisibility(R.id.big_text, View.VISIBLE); diff --git a/core/java/android/nfc/INfcTag.aidl b/core/java/android/nfc/INfcTag.aidl index 102b6af..2223255 100644 --- a/core/java/android/nfc/INfcTag.aidl +++ b/core/java/android/nfc/INfcTag.aidl @@ -29,7 +29,6 @@ interface INfcTag int connect(int nativeHandle, int technology); int reconnect(int nativeHandle); int[] getTechList(int nativeHandle); - byte[] getUid(int nativeHandle); boolean isNdef(int nativeHandle); boolean isPresent(int nativeHandle); TransceiveResult transceive(int nativeHandle, in byte[] data, boolean raw); diff --git a/core/java/android/nfc/tech/Ndef.java b/core/java/android/nfc/tech/Ndef.java index b1d5303..a31cb9c 100644 --- a/core/java/android/nfc/tech/Ndef.java +++ b/core/java/android/nfc/tech/Ndef.java @@ -256,6 +256,8 @@ public final class Ndef extends BasicTagTechnology { * not be called from the main application thread. A blocked call will be canceled with * {@link IOException} if {@link #close} is called from another thread. * + * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. + * * @return the NDEF Message, can be null * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or the operation is canceled diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java index b816b11..4aa7fe2 100644 --- a/core/java/android/os/Message.java +++ b/core/java/android/os/Message.java @@ -16,9 +16,6 @@ package android.os; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; import android.util.TimeUtils; /** @@ -368,13 +365,13 @@ public final class Message implements Parcelable { * * Asynchronous messages represent interrupts or events that do not require global ordering * with represent to synchronous messages. Asynchronous messages are not subject to - * the synchronization barriers introduced by {@link MessageQueue#acquireSyncBarrier()}. + * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}. * * @return True if the message is asynchronous. * * @see #setAsynchronous(boolean) - * @see MessageQueue#acquireSyncBarrier() - * @see MessageQueue#releaseSyncBarrier() + * @see MessageQueue#enqueueSyncBarrier(long) + * @see MessageQueue#removeSyncBarrier(int) * * @hide */ @@ -387,13 +384,13 @@ public final class Message implements Parcelable { * * Asynchronous messages represent interrupts or events that do not require global ordering * with represent to synchronous messages. Asynchronous messages are not subject to - * the synchronization barriers introduced by {@link MessageQueue#acquireSyncBarrier()}. + * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}. * * @param async True if the message is asynchronous. * * @see #isAsynchronous() - * @see MessageQueue#acquireSyncBarrier() - * @see MessageQueue#releaseSyncBarrier() + * @see MessageQueue#enqueueSyncBarrier(long) + * @see MessageQueue#removeSyncBarrier(int) * * @hide */ @@ -506,7 +503,7 @@ public final class Message implements Parcelable { Messenger.writeMessengerOrNullToParcel(replyTo, dest); } - private final void readFromParcel(Parcel source) { + private void readFromParcel(Parcel source) { what = source.readInt(); arg1 = source.readInt(); arg2 = source.readInt(); diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index b708750..f7a7eb8 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -262,19 +262,8 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return append(String.valueOf(text)); } - private int change(int start, int end, CharSequence tb, int tbstart, int tbend) { - return change(true, start, end, tb, tbstart, tbend); - } - - private int change(boolean notify, int start, int end, - CharSequence tb, int tbstart, int tbend) { + private void change(int start, int end, CharSequence tb, int tbstart, int tbend) { checkRange("replace", start, end); - int ret = tbend - tbstart; - TextWatcher[] recipients = null; - - if (notify) { - recipients = sendTextWillChange(start, end - start, tbend - tbstart); - } for (int i = mSpanCount - 1; i >= 0; i--) { if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { @@ -346,51 +335,37 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } } - // no need for span fixup on pure insertion - if (tbend > tbstart && end - start == 0) { - if (notify) { - sendTextChange(recipients, start, end - start, tbend - tbstart); - sendTextHasChanged(recipients); - } - - return ret; - } - - boolean atend = (mGapStart + mGapLength == mText.length); + if (end > start) { + // no need for span fixup on pure insertion + boolean atEnd = (mGapStart + mGapLength == mText.length); - for (int i = mSpanCount - 1; i >= 0; i--) { - if (mSpanStarts[i] >= start && - mSpanStarts[i] < mGapStart + mGapLength) { - int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; + for (int i = mSpanCount - 1; i >= 0; i--) { + if (mSpanStarts[i] >= start && + mSpanStarts[i] < mGapStart + mGapLength) { + int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; - if (flag == POINT || (flag == PARAGRAPH && atend)) + if (flag == POINT || (flag == PARAGRAPH && atEnd)) mSpanStarts[i] = mGapStart + mGapLength; else mSpanStarts[i] = start; - } + } - if (mSpanEnds[i] >= start && - mSpanEnds[i] < mGapStart + mGapLength) { - int flag = (mSpanFlags[i] & END_MASK); + if (mSpanEnds[i] >= start && + mSpanEnds[i] < mGapStart + mGapLength) { + int flag = (mSpanFlags[i] & END_MASK); - if (flag == POINT || (flag == PARAGRAPH && atend)) - mSpanEnds[i] = mGapStart + mGapLength; - else - mSpanEnds[i] = start; - } + if (flag == POINT || (flag == PARAGRAPH && atEnd)) + mSpanEnds[i] = mGapStart + mGapLength; + else + mSpanEnds[i] = start; + } - // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE - if (mSpanEnds[i] < mSpanStarts[i]) { - removeSpan(i); + // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE + if (mSpanEnds[i] < mSpanStarts[i]) { + removeSpan(i); + } } } - - if (notify) { - sendTextChange(recipients, start, end - start, tbend - tbstart); - sendTextHasChanged(recipients); - } - - return ret; } private void removeSpan(int i) { @@ -425,8 +400,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable CharSequence tb, int tbstart, int tbend) { int filtercount = mFilters.length; for (int i = 0; i < filtercount; i++) { - CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, - this, start, end); + CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end); if (repl != null) { tb = repl; @@ -435,11 +409,17 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } } - if (end == start && tbstart == tbend) { + final int origLen = end - start; + final int newLen = tbend - tbstart; + + if (origLen == 0 && newLen == 0) { return this; } - if (end == start || tbstart == tbend) { + TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class); + sendBeforeTextChanged(textWatchers, start, origLen, newLen); + + if (origLen == 0 || newLen == 0) { change(start, end, tb, tbstart, tbend); } else { int selstart = Selection.getSelectionStart(this); @@ -450,11 +430,6 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable checkRange("replace", start, end); moveGapTo(end); - TextWatcher[] recipients; - - int origlen = end - start; - - recipients = sendTextWillChange(start, origlen, tbend - tbstart); if (mGapLength < 2) resizeFor(length() + 1); @@ -475,9 +450,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable new Exception("mGapLength < 1").printStackTrace(); } - int inserted = change(false, start + 1, start + 1, tb, tbstart, tbend); - change(false, start, start + 1, "", 0, 0); - change(false, start + inserted, start + inserted + origlen, "", 0, 0); + change(start + 1, start + 1, tb, tbstart, tbend); + change(start, start + 1, "", 0, 0); + change(start + newLen, start + newLen + origLen, "", 0, 0); /* * Special case to keep the cursor in the same position @@ -490,7 +465,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (selstart > start && selstart < end) { long off = selstart - start; - off = off * inserted / (end - start); + off = off * newLen / (end - start); selstart = (int) off + start; setSpan(false, Selection.SELECTION_START, selstart, selstart, @@ -499,15 +474,16 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (selend > start && selend < end) { long off = selend - start; - off = off * inserted / (end - start); + off = off * newLen / (end - start); selend = (int) off + start; setSpan(false, Selection.SELECTION_END, selend, selend, Spanned.SPAN_POINT_POINT); } - sendTextChange(recipients, start, origlen, inserted); - sendTextHasChanged(recipients); } + sendTextChanged(textWatchers, start, origLen, newLen); + sendAfterTextChanged(textWatchers); + return this; } @@ -579,8 +555,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanEnds[i] = end; mSpanFlags[i] = flags; - if (send) - sendSpanChanged(what, ostart, oend, nstart, nend); + if (send) sendSpanChanged(what, ostart, oend, nstart, nend); return; } @@ -610,8 +585,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanFlags[mSpanCount] = flags; mSpanCount++; - if (send) - sendSpanAdded(what, nstart, nend); + if (send) sendSpanAdded(what, nstart, nend); } /** @@ -874,30 +848,27 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return new String(buf); } - private TextWatcher[] sendTextWillChange(int start, int before, int after) { - TextWatcher[] recip = getSpans(start, start + before, TextWatcher.class); - int n = recip.length; + private void sendBeforeTextChanged(TextWatcher[] watchers, int start, int before, int after) { + int n = watchers.length; for (int i = 0; i < n; i++) { - recip[i].beforeTextChanged(this, start, before, after); + watchers[i].beforeTextChanged(this, start, before, after); } - - return recip; } - private void sendTextChange(TextWatcher[] recip, int start, int before, int after) { - int n = recip.length; + private void sendTextChanged(TextWatcher[] watchers, int start, int before, int after) { + int n = watchers.length; for (int i = 0; i < n; i++) { - recip[i].onTextChanged(this, start, before, after); + watchers[i].onTextChanged(this, start, before, after); } } - private void sendTextHasChanged(TextWatcher[] recip) { - int n = recip.length; + private void sendAfterTextChanged(TextWatcher[] watchers) { + int n = watchers.length; for (int i = 0; i < n; i++) { - recip[i].afterTextChanged(this); + watchers[i].afterTextChanged(this); } } @@ -1039,8 +1010,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * Don't call this yourself -- exists for Canvas to use internally. * {@hide} */ - public void drawText(Canvas c, int start, int end, - float x, float y, Paint p) { + public void drawText(Canvas c, int start, int end, float x, float y, Paint p) { checkRange("drawText", start, end); if (end <= mGapStart) { @@ -1061,8 +1031,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * Don't call this yourself -- exists for Canvas to use internally. * {@hide} */ - public void drawTextRun(Canvas c, int start, int end, - int contextStart, int contextEnd, + public void drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd, float x, float y, int flags, Paint p) { checkRange("drawTextRun", start, end); @@ -1264,6 +1233,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private int[] mSpanFlags; private int mSpanCount; + // TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned} private static final int POINT = 2; private static final int PARAGRAPH = 3; diff --git a/core/java/android/view/DisplayList.java b/core/java/android/view/DisplayList.java index e2aafa9..33631b7 100644 --- a/core/java/android/view/DisplayList.java +++ b/core/java/android/view/DisplayList.java @@ -16,6 +16,8 @@ package android.view; +import android.graphics.Matrix; + /** * A display lists records a series of graphics related operation and can replay * them later. Display lists are usually built by recording operations on a @@ -117,12 +119,26 @@ public abstract class DisplayList { public abstract void setClipChildren(boolean clipChildren); /** - * Set the application scale on the DisplayList. This scale is incurred by applications that - * are auto-scaled for compatibility reasons. By default, the value is 1 (unscaled). + * Set the static matrix on the DisplayList. This matrix exists if a custom ViewGroup + * overrides + * {@link ViewGroup#getChildStaticTransformation(View, android.view.animation.Transformation)} + * and also has {@link ViewGroup#setStaticTransformationsEnabled(boolean)} set to true. + * This matrix will be concatenated with any other matrices in the DisplayList to position + * the view appropriately. + * + * @param matrix The matrix + */ + public abstract void setStaticMatrix(Matrix matrix); + + /** + * Set the Animation matrix on the DisplayList. This matrix exists if an Animation is + * currently playing on a View, and is set on the DisplayList during at draw() time. When + * the Animation finishes, the matrix should be cleared by sending <code>null</code> + * for the matrix parameter. * - * @param scale The scaling factor + * @param matrix The matrix, null indicates that the matrix should be cleared. */ - public abstract void setApplicationScale(float scale); + public abstract void setAnimationMatrix(Matrix matrix); /** * Sets the alpha value for the DisplayList diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 5b0433e..bedafc7 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -259,6 +259,13 @@ class GLES20Canvas extends HardwareCanvas { private static native int nCallDrawGLFunction(int renderer, int drawGLFunction); + @Override + public int invokeFunctors(Rect dirty) { + return nInvokeFunctors(mRenderer, dirty); + } + + private static native int nInvokeFunctors(int renderer, Rect dirty); + /////////////////////////////////////////////////////////////////////////// // Memory /////////////////////////////////////////////////////////////////////////// diff --git a/core/java/android/view/GLES20DisplayList.java b/core/java/android/view/GLES20DisplayList.java index 9b4cf21..bc3bce0 100644 --- a/core/java/android/view/GLES20DisplayList.java +++ b/core/java/android/view/GLES20DisplayList.java @@ -17,6 +17,7 @@ package android.view; import android.graphics.Bitmap; +import android.graphics.Matrix; import java.util.ArrayList; @@ -119,9 +120,18 @@ class GLES20DisplayList extends DisplayList { } @Override - public void setApplicationScale(float scale) { + public void setStaticMatrix(Matrix matrix) { try { - nSetApplicationScale(getNativeDisplayList(), scale); + nSetStaticMatrix(getNativeDisplayList(), matrix.native_instance); + } catch (IllegalStateException e) { + // invalid DisplayList okay: we'll set current values the next time we render to it + } + } + + @Override + public void setAnimationMatrix(Matrix matrix) { + try { + nSetAnimationMatrix(getNativeDisplayList(), matrix.native_instance); } catch (IllegalStateException e) { // invalid DisplayList okay: we'll set current values the next time we render to it } @@ -335,6 +345,8 @@ class GLES20DisplayList extends DisplayList { private static native void nSetTransformationInfo(int displayList, float alpha, float translationX, float translationY, float rotation, float rotationX, float rotationY, float scaleX, float scaleY); + private static native void nSetStaticMatrix(int displayList, int nativeMatrix); + private static native void nSetAnimationMatrix(int displayList, int animationMatrix); /////////////////////////////////////////////////////////////////////////// diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java index 2636ea2..de8c62d 100644 --- a/core/java/android/view/HardwareCanvas.java +++ b/core/java/android/view/HardwareCanvas.java @@ -98,4 +98,16 @@ public abstract class HardwareCanvas extends Canvas { // Noop - this is done in the display list recorder subclass return DisplayList.STATUS_DONE; } + + /** + * Invoke all the functors who requested to be invoked during the previous frame. + * + * @param dirty The region to redraw when the functors return {@link DisplayList#STATUS_DRAW} + * + * @return One of {@link DisplayList#STATUS_DONE}, {@link DisplayList#STATUS_DRAW} or + * {@link DisplayList#STATUS_INVOKE} + */ + public int invokeFunctors(Rect dirty) { + return DisplayList.STATUS_DONE; + } } diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index 81c7ebf..b100a0c 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -259,7 +259,14 @@ public abstract class HardwareRenderer { * @param pw */ abstract void dumpGfxInfo(PrintWriter pw); - + + /** + * Outputs the total number of frames rendered (used for fps calculations) + * + * @return the number of frames rendered + */ + abstract long getFrameCount(); + /** * Sets the directory to use as a persistent storage for hardware rendering * resources. @@ -274,12 +281,13 @@ public abstract class HardwareRenderer { /** * Notifies EGL that the frame is about to be rendered. + * @param size */ - private static void beginFrame() { - nBeginFrame(); + private static void beginFrame(int[] size) { + nBeginFrame(size); } - private static native void nBeginFrame(); + private static native void nBeginFrame(int[] size); /** * Preserves the back buffer of the current surface after a buffer swap. @@ -494,7 +502,9 @@ public abstract class HardwareRenderer { static final int SURFACE_STATE_ERROR = 0; static final int SURFACE_STATE_SUCCESS = 1; static final int SURFACE_STATE_UPDATED = 2; - + + static final int FUNCTOR_PROCESS_DELAY = 2; + static EGL10 sEgl; static EGLDisplay sEglDisplay; static EGLConfig sEglConfig; @@ -512,7 +522,7 @@ public abstract class HardwareRenderer { GL mGl; HardwareCanvas mCanvas; - int mFrameCount; + long mFrameCount; Paint mDebugPaint; static boolean sDirtyRegions; @@ -542,6 +552,9 @@ public abstract class HardwareRenderer { private final Rect mRedrawClip = new Rect(); + private final int[] mSurfaceSize = new int[2]; + private final FunctorsRunnable mFunctorsRunnable = new FunctorsRunnable(); + GlRenderer(int glVersion, boolean translucent) { mGlVersion = glVersion; mTranslucent = translucent; @@ -589,6 +602,11 @@ public abstract class HardwareRenderer { } } + @Override + long getFrameCount() { + return mFrameCount; + } + /** * Indicates whether this renderer instance can track and update dirty regions. */ @@ -943,6 +961,24 @@ public abstract class HardwareRenderer { void onPostDraw() { } + class FunctorsRunnable implements Runnable { + View.AttachInfo attachInfo; + + @Override + public void run() { + final HardwareRenderer renderer = attachInfo.mHardwareRenderer; + if (renderer == null || !renderer.isEnabled() || renderer != GlRenderer.this) { + return; + } + + final int surfaceState = checkCurrent(); + if (surfaceState != SURFACE_STATE_ERROR) { + int status = mCanvas.invokeFunctors(mRedrawClip); + handleFunctorStatus(attachInfo, status); + } + } + } + @Override boolean draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, Rect dirty) { @@ -957,17 +993,29 @@ public abstract class HardwareRenderer { final int surfaceState = checkCurrent(); if (surfaceState != SURFACE_STATE_ERROR) { + HardwareCanvas canvas = mCanvas; + attachInfo.mHardwareCanvas = canvas; + // We had to change the current surface and/or context, redraw everything if (surfaceState == SURFACE_STATE_UPDATED) { dirty = null; + beginFrame(null); + } else { + int[] size = mSurfaceSize; + beginFrame(size); + + if (size[1] != mHeight || size[0] != mWidth) { + mWidth = size[0]; + mHeight = size[1]; + + canvas.setViewport(mWidth, mHeight); + + dirty = null; + } } - beginFrame(); onPreDraw(dirty); - HardwareCanvas canvas = mCanvas; - attachInfo.mHardwareCanvas = canvas; - int saveCount = canvas.save(); callbacks.onHardwarePreDraw(canvas); @@ -1025,15 +1073,7 @@ public abstract class HardwareRenderer { } } - if (status != DisplayList.STATUS_DONE) { - if (mRedrawClip.isEmpty()) { - attachInfo.mViewRootImpl.invalidate(); - } else { - attachInfo.mViewRootImpl.invalidateChildInParent( - null, mRedrawClip); - mRedrawClip.setEmpty(); - } - } + handleFunctorStatus(attachInfo, status); } else { // Shouldn't reach here view.draw(canvas); @@ -1042,13 +1082,13 @@ public abstract class HardwareRenderer { callbacks.onHardwarePostDraw(canvas); canvas.restoreToCount(saveCount); view.mRecreateDisplayList = false; - + mFrameCount++; if (mDebugDirtyRegions) { if (mDebugPaint == null) { mDebugPaint = new Paint(); mDebugPaint.setColor(0x7fff0000); } - if (dirty != null && (mFrameCount++ & 1) == 0) { + if (dirty != null && (mFrameCount & 1) == 0) { canvas.drawRect(dirty, mDebugPaint); } } @@ -1085,6 +1125,26 @@ public abstract class HardwareRenderer { return false; } + private void handleFunctorStatus(View.AttachInfo attachInfo, int status) { + // If the draw flag is set, functors will be invoked while executing + // the tree of display lists + if ((status & DisplayList.STATUS_DRAW) != 0) { + if (mRedrawClip.isEmpty()) { + attachInfo.mViewRootImpl.invalidate(); + } else { + attachInfo.mViewRootImpl.invalidateChildInParent(null, mRedrawClip); + mRedrawClip.setEmpty(); + } + } + + if ((status & DisplayList.STATUS_INVOKE) != 0) { + attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); + mFunctorsRunnable.attachInfo = attachInfo; + // delay the functor callback by a few ms so it isn't polled constantly + attachInfo.mHandler.postDelayed(mFunctorsRunnable, FUNCTOR_PROCESS_DELAY); + } + } + /** * Ensures the current EGL context is the one we expect. * diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 104ed6a..e4a4a75 100755 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -591,8 +591,24 @@ public class KeyEvent extends InputEvent implements Parcelable { /** Key code constant: Calculator special function key. * Used to launch a calculator application. */ public static final int KEYCODE_CALCULATOR = 210; - - private static final int LAST_KEYCODE = KEYCODE_CALCULATOR; + /** Key code constant: Japanese full-width / half-width key. */ + public static final int KEYCODE_ZENKAKU_HANKAKU = 211; + /** Key code constant: Japanese alphanumeric key. */ + public static final int KEYCODE_EISU = 212; + /** Key code constant: Japanese non-conversion key. */ + public static final int KEYCODE_MUHENKAN = 213; + /** Key code constant: Japanese conversion key. */ + public static final int KEYCODE_HENKAN = 214; + /** Key code constant: Japanese katakana / hiragana key. */ + public static final int KEYCODE_KATAKANA_HIRAGANA = 215; + /** Key code constant: Japanese Yen key. */ + public static final int KEYCODE_YEN = 216; + /** Key code constant: Japanese Ro key. */ + public static final int KEYCODE_RO = 217; + /** Key code constant: Japanese kana key. */ + public static final int KEYCODE_KANA = 218; + + private static final int LAST_KEYCODE = KEYCODE_KANA; // NOTE: If you add a new keycode here you must also add it to: // isSystem() @@ -825,6 +841,14 @@ public class KeyEvent extends InputEvent implements Parcelable { names.append(KEYCODE_CALENDAR, "KEYCODE_CALENDAR"); names.append(KEYCODE_MUSIC, "KEYCODE_MUSIC"); names.append(KEYCODE_CALCULATOR, "KEYCODE_CALCULATOR"); + names.append(KEYCODE_ZENKAKU_HANKAKU, "KEYCODE_ZENKAKU_HANKAKU"); + names.append(KEYCODE_EISU, "KEYCODE_EISU"); + names.append(KEYCODE_MUHENKAN, "KEYCODE_MUHENKAN"); + names.append(KEYCODE_HENKAN, "KEYCODE_HENKAN"); + names.append(KEYCODE_KATAKANA_HIRAGANA, "KEYCODE_KATAKANA_HIRAGANA"); + names.append(KEYCODE_YEN, "KEYCODE_YEN"); + names.append(KEYCODE_RO, "KEYCODE_RO"); + names.append(KEYCODE_KANA, "KEYCODE_KANA"); }; // Symbolic names of all metakeys in bit order from least significant to most significant. diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 6c964b0..18e1697 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -765,7 +765,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ static final int FILTER_TOUCHES_WHEN_OBSCURED = 0x00000400; - // note flag value 0x00000800 is now available for next flags... + /** + * Set for framework elements that use FITS_SYSTEM_WINDOWS, to indicate + * that they are optional and should be skipped if the window has + * requested system UI flags that ignore those insets for layout. + */ + static final int OPTIONAL_FITS_SYSTEM_WINDOWS = 0x00000800; /** * <p>This view doesn't show fading edges.</p> @@ -1459,7 +1464,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * apps. * @hide */ - public static final boolean USE_DISPLAY_LIST_PROPERTIES = false; + public static final boolean USE_DISPLAY_LIST_PROPERTIES = true; /** * Map used to store views' tags. @@ -1909,28 +1914,31 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int OVER_SCROLL_NEVER = 2; /** - * View has requested the system UI (status bar) to be visible (the default). + * Special constant for {@link #setSystemUiVisibility(int)}: View has + * requested the system UI (status bar) to be visible (the default). * * @see #setSystemUiVisibility(int) */ public static final int SYSTEM_UI_FLAG_VISIBLE = 0; /** - * View has requested the system UI to enter an unobtrusive "low profile" mode. + * Flag for {@link #setSystemUiVisibility(int)}: View has requested the + * system UI to enter an unobtrusive "low profile" mode. * - * This is for use in games, book readers, video players, or any other "immersive" application - * where the usual system chrome is deemed too distracting. + * <p>This is for use in games, book readers, video players, or any other + * "immersive" application where the usual system chrome is deemed too distracting. * - * In low profile mode, the status bar and/or navigation icons may dim. + * <p>In low profile mode, the status bar and/or navigation icons may dim. * * @see #setSystemUiVisibility(int) */ public static final int SYSTEM_UI_FLAG_LOW_PROFILE = 0x00000001; /** - * View has requested that the system navigation be temporarily hidden. + * Flag for {@link #setSystemUiVisibility(int)}: View has requested that the + * system navigation be temporarily hidden. * - * This is an even less obtrusive state than that called for by + * <p>This is an even less obtrusive state than that called for by * {@link #SYSTEM_UI_FLAG_LOW_PROFILE}; on devices that draw essential navigation controls * (Home, Back, and the like) on screen, <code>SYSTEM_UI_FLAG_HIDE_NAVIGATION</code> will cause * those to disappear. This is useful (in conjunction with the @@ -1938,14 +1946,92 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * {@link android.view.WindowManager.LayoutParams#FLAG_LAYOUT_IN_SCREEN FLAG_LAYOUT_IN_SCREEN} * window flags) for displaying content using every last pixel on the display. * - * There is a limitation: because navigation controls are so important, the least user - * interaction will cause them to reappear immediately. + * <p>There is a limitation: because navigation controls are so important, the least user + * interaction will cause them to reappear immediately. When this happens, both + * this flag and {@link #SYSTEM_UI_FLAG_FULLSCREEN} will be cleared automatically, + * so that both elements reappear at the same time. * * @see #setSystemUiVisibility(int) */ public static final int SYSTEM_UI_FLAG_HIDE_NAVIGATION = 0x00000002; /** + * Flag for {@link #setSystemUiVisibility(int)}: View has requested to go + * into the normal fullscreen mode so that its content can take over the screen + * while still allowing the user to interact with the application. + * + * <p>This has the same visual effect as + * {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN + * WindowManager.LayoutParams.FLAG_FULLSCREEN}, + * meaning that non-critical screen decorations (such as the status bar) will be + * hidden while the user is in the View's window, focusing the experience on + * that content. Unlike the window flag, if you are using ActionBar in + * overlay mode with {@link Window#FEATURE_ACTION_BAR_OVERLAY + * Window.FEATURE_ACTION_BAR_OVERLAY}, then enabling this flag will also + * hide the action bar. + * + * <p>This approach to going fullscreen is best used over the window flag when + * it is a transient state -- that is, the application does this at certain + * points in its user interaction where it wants to allow the user to focus + * on content, but not as a continuous state. For situations where the application + * would like to simply stay full screen the entire time (such as a game that + * wants to take over the screen), the + * {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN window flag} + * is usually a better approach. The state set here will be removed by the system + * in various situations (such as the user moving to another application) like + * the other system UI states. + * + * <p>When using this flag, the application should provide some easy facility + * for the user to go out of it. A common example would be in an e-book + * reader, where tapping on the screen brings back whatever screen and UI + * decorations that had been hidden while the user was immersed in reading + * the book. + * + * @see #setSystemUiVisibility(int) + */ + public static final int SYSTEM_UI_FLAG_FULLSCREEN = 0x00000004; + + /** + * Flag for {@link #setSystemUiVisibility(int)}: When using other layout + * flags, we would like a stable view of the content insets given to + * {@link #fitSystemWindows(Rect)}. This means that the insets seen there + * will always represent the worst case that the application can expect + * as a continue state. In practice this means with any of system bar, + * nav bar, and status bar shown, but not the space that would be needed + * for an input method. + * + * <p>If you are using ActionBar in + * overlay mode with {@link Window#FEATURE_ACTION_BAR_OVERLAY + * Window.FEATURE_ACTION_BAR_OVERLAY}, this flag will also impact the + * insets it adds to those given to the application. + */ + public static final int SYSTEM_UI_FLAG_LAYOUT_STABLE = 0x00000100; + + /** + * Flag for {@link #setSystemUiVisibility(int)}: View would like its window + * to be layed out as if it has requested + * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, even if it currently hasn't. This + * allows it to avoid artifacts when switching in and out of that mode, at + * the expense that some of its user interface may be covered by screen + * decorations when they are shown. You can perform layout of your inner + * UI elements to account for the navagation system UI through the + * {@link #fitSystemWindows(Rect)} method. + */ + public static final int SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 0x00000200; + + /** + * Flag for {@link #setSystemUiVisibility(int)}: View would like its window + * to be layed out as if it has requested + * {@link #SYSTEM_UI_FLAG_FULLSCREEN}, even if it currently hasn't. This + * allows it to avoid artifacts when switching in and out of that mode, at + * the expense that some of its user interface may be covered by screen + * decorations when they are shown. You can perform layout of your inner + * UI elements to account for non-fullscreen system UI through the + * {@link #fitSystemWindows(Rect)} method. + */ + public static final int SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400; + + /** * @deprecated Use {@link #SYSTEM_UI_FLAG_LOW_PROFILE} instead. */ public static final int STATUS_BAR_HIDDEN = SYSTEM_UI_FLAG_LOW_PROFILE; @@ -2055,17 +2141,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal /** * @hide - * - * NOTE: This flag may only be used in subtreeSystemUiVisibility, etc. etc. - * - * This hides HOME and RECENT and is provided for compatibility with interim implementations. - */ - @Deprecated - public static final int STATUS_BAR_DISABLE_NAVIGATION = - STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT; - - /** - * @hide */ public static final int PUBLIC_STATUS_BAR_VISIBILITY_MASK = 0x0000FFFF; @@ -2076,7 +2151,15 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * @hide */ public static final int SYSTEM_UI_CLEARABLE_FLAGS = - SYSTEM_UI_FLAG_LOW_PROFILE | SYSTEM_UI_FLAG_HIDE_NAVIGATION; + SYSTEM_UI_FLAG_LOW_PROFILE | SYSTEM_UI_FLAG_HIDE_NAVIGATION + | SYSTEM_UI_FLAG_FULLSCREEN; + + /** + * Flags that can impact the layout in relation to system UI. + */ + public static final int SYSTEM_UI_LAYOUT_FLAGS = + SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; /** * Find views that render the specified text. @@ -4692,21 +4775,54 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** - * Apply the insets for system windows to this view, if the FITS_SYSTEM_WINDOWS flag - * is set + * Called by the view hierarchy when the content insets for a window have + * changed, to allow it to adjust its content to fit within those windows. + * The content insets tell you the space that the status bar, input method, + * and other system windows infringe on the application's window. + * + * <p>You do not normally need to deal with this function, since the default + * window decoration given to applications takes care of applying it to the + * content of the window. If you use {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} + * or {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION} this will not be the case, + * and your content can be placed under those system elements. You can then + * use this method within your view hierarchy if you have parts of your UI + * which you would like to ensure are not being covered. * - * @param insets Insets for system windows + * <p>The default implementation of this method simply applies the content + * inset's to the view's padding. This can be enabled through + * {@link #setFitsSystemWindows(boolean)}. Alternatively, you can override + * the method and handle the insets however you would like. Note that the + * insets provided by the framework are always relative to the far edges + * of the window, not accounting for the location of the called view within + * that window. (In fact when this method is called you do not yet know + * where the layout will place the view, as it is done before layout happens.) * - * @return True if this view applied the insets, false otherwise + * <p>Note: unlike many View methods, there is no dispatch phase to this + * call. If you are overriding it in a ViewGroup and want to allow the + * call to continue to your children, you must be sure to call the super + * implementation. + * + * @param insets Current content insets of the window. Prior to + * {@link android.os.Build.VERSION_CODES#JELLY_BEAN} you must not modify + * the insets or else you and Android will be unhappy. + * + * @return Return true if this view applied the insets and it should not + * continue propagating further down the hierarchy, false otherwise. */ protected boolean fitSystemWindows(Rect insets) { if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) { - mPaddingLeft = insets.left; - mPaddingTop = insets.top; - mPaddingRight = insets.right; - mPaddingBottom = insets.bottom; - requestLayout(); - return true; + mUserPaddingStart = -1; + mUserPaddingEnd = -1; + mUserPaddingRelative = false; + if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0 + || mAttachInfo == null + || (mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0) { + internalSetPadding(insets.left, insets.top, insets.right, insets.bottom); + return true; + } else { + internalSetPadding(0, 0, 0, 0); + return false; + } } return false; } @@ -4742,6 +4858,23 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Ask that a new dispatch of {@link #fitSystemWindows(Rect)} be performed. + */ + public void requestFitSystemWindows() { + if (mParent != null) { + mParent.requestFitSystemWindows(); + } + } + + /** + * For use by PhoneWindow to make its own system window fitting optional. + * @hide + */ + public void makeOptionalFitsSystemWindows() { + setFlags(OPTIONAL_FITS_SYSTEM_WINDOWS, OPTIONAL_FITS_SYSTEM_WINDOWS); + } + + /** * Returns the visibility status for this view. * * @return One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. @@ -6118,19 +6251,19 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * Private function to aggregate all per-view attributes in to the view * root. */ - void dispatchCollectViewAttributes(int visibility) { - performCollectViewAttributes(visibility); + void dispatchCollectViewAttributes(AttachInfo attachInfo, int visibility) { + performCollectViewAttributes(attachInfo, visibility); } - void performCollectViewAttributes(int visibility) { - if ((visibility & VISIBILITY_MASK) == VISIBLE && mAttachInfo != null) { + void performCollectViewAttributes(AttachInfo attachInfo, int visibility) { + if ((visibility & VISIBILITY_MASK) == VISIBLE) { if ((mViewFlags & KEEP_SCREEN_ON) == KEEP_SCREEN_ON) { - mAttachInfo.mKeepScreenOn = true; + attachInfo.mKeepScreenOn = true; } - mAttachInfo.mSystemUiVisibility |= mSystemUiVisibility; + attachInfo.mSystemUiVisibility |= mSystemUiVisibility; ListenerInfo li = mListenerInfo; if (li != null && li.mOnSystemUiVisibilityChangeListener != null) { - mAttachInfo.mHasSystemUiListeners = true; + attachInfo.mHasSystemUiListeners = true; } } } @@ -10126,7 +10259,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal mAttachInfo.mScrollContainers.add(this); mPrivateFlags |= SCROLL_CONTAINER_ADDED; } - performCollectViewAttributes(visibility); + performCollectViewAttributes(mAttachInfo, visibility); onAttachedToWindow(); ListenerInfo li = mListenerInfo; @@ -11396,12 +11529,34 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal displayList.setClipChildren( (((ViewGroup)mParent).mGroupFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0); } - if (mAttachInfo != null && mAttachInfo.mScalingRequired && - mAttachInfo.mApplicationScale != 1.0f) { - displayList.setApplicationScale(1f / mAttachInfo.mApplicationScale); + float alpha = 1; + if (mParent instanceof ViewGroup && (((ViewGroup) mParent).mGroupFlags & + ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) { + ViewGroup parentVG = (ViewGroup) mParent; + final boolean hasTransform = + parentVG.getChildStaticTransformation(this, parentVG.mChildTransformation); + if (hasTransform) { + Transformation transform = parentVG.mChildTransformation; + final int transformType = parentVG.mChildTransformation.getTransformationType(); + if (transformType != Transformation.TYPE_IDENTITY) { + if ((transformType & Transformation.TYPE_ALPHA) != 0) { + alpha = transform.getAlpha(); + } + if ((transformType & Transformation.TYPE_MATRIX) != 0) { + displayList.setStaticMatrix(transform.getMatrix()); + } + } + } } if (mTransformationInfo != null) { - displayList.setTransformationInfo(mTransformationInfo.mAlpha, + alpha *= mTransformationInfo.mAlpha; + if (alpha < 1) { + final int multipliedAlpha = (int) (255 * alpha); + if (onSetAlpha(multipliedAlpha)) { + alpha = 1; + } + } + displayList.setTransformationInfo(alpha, mTransformationInfo.mTranslationX, mTransformationInfo.mTranslationY, mTransformationInfo.mRotation, mTransformationInfo.mRotationX, mTransformationInfo.mRotationY, mTransformationInfo.mScaleX, @@ -11415,6 +11570,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal displayList.setPivotX(getPivotX()); displayList.setPivotY(getPivotY()); } + } else if (alpha < 1) { + displayList.setAlpha(alpha); } } } @@ -11447,6 +11604,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal if ((flags & ViewGroup.FLAG_CHILDREN_DRAWN_WITH_CACHE) != 0 || (flags & ViewGroup.FLAG_ALWAYS_DRAWN_WITH_CACHE) != 0) { caching = true; + // Auto-scaled apps are not hw-accelerated, no need to set scaling flag on DisplayList if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired; } else { caching = (layerType != LAYER_TYPE_NONE) || hardwareAccelerated; @@ -11457,7 +11615,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal more = drawAnimation(parent, drawingTime, a, scalingRequired); concatMatrix = a.willChangeTransformationMatrix(); transformToApply = parent.mChildTransformation; - } else if ((flags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) { + } else if (!useDisplayListProperties && + (flags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) { final boolean hasTransform = parent.getChildStaticTransformation(this, parent.mChildTransformation); if (hasTransform) { @@ -11525,6 +11684,17 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } } useDisplayListProperties &= hasDisplayList; + if (useDisplayListProperties) { + displayList = getDisplayList(); + if (!displayList.isValid()) { + // Uncommon, but possible. If a view is removed from the hierarchy during the call + // to getDisplayList(), the display list will be marked invalid and we should not + // try to use it again. + displayList = null; + hasDisplayList = false; + useDisplayListProperties = false; + } + } final boolean hasNoCache = cache == null || hasDisplayList; final boolean offsetForScroll = cache == null && !hasDisplayList && @@ -11542,6 +11712,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } if (scalingRequired) { if (useDisplayListProperties) { + // TODO: Might not need this if we put everything inside the DL restoreTo = canvas.save(); } // mAttachInfo cannot be null, otherwise scalingRequired == false @@ -11551,7 +11722,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } float alpha = useDisplayListProperties ? 1 : getAlpha(); - if (transformToApply != null || alpha < 1.0f || !hasIdentityMatrix()) { + if (transformToApply != null || alpha < 1 || !hasIdentityMatrix()) { if (transformToApply != null || !childHasIdentityMatrix) { int transX = 0; int transY = 0; @@ -11563,16 +11734,20 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal if (transformToApply != null) { if (concatMatrix) { - // Undo the scroll translation, apply the transformation matrix, - // then redo the scroll translate to get the correct result. - canvas.translate(-transX, -transY); - canvas.concat(transformToApply.getMatrix()); - canvas.translate(transX, transY); + if (useDisplayListProperties) { + displayList.setAnimationMatrix(transformToApply.getMatrix()); + } else { + // Undo the scroll translation, apply the transformation matrix, + // then redo the scroll translate to get the correct result. + canvas.translate(-transX, -transY); + canvas.concat(transformToApply.getMatrix()); + canvas.translate(transX, transY); + } parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION; } float transformAlpha = transformToApply.getAlpha(); - if (transformAlpha < 1.0f) { + if (transformAlpha < 1) { alpha *= transformToApply.getAlpha(); parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION; } @@ -11585,7 +11760,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } } - if (alpha < 1.0f) { + if (alpha < 1) { parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION; if (hasNoCache) { final int multipliedAlpha = (int) (255 * alpha); @@ -11595,7 +11770,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal layerType != LAYER_TYPE_NONE) { layerFlags |= Canvas.CLIP_TO_LAYER_SAVE_FLAG; } - if (layerType == LAYER_TYPE_NONE) { + if (useDisplayListProperties) { + displayList.setAlpha(alpha * getAlpha()); + } else if (layerType == LAYER_TYPE_NONE) { final int scrollX = hasDisplayList ? 0 : sx; final int scrollY = hasDisplayList ? 0 : sy; canvas.saveLayerAlpha(scrollX, scrollY, scrollX + mRight - mLeft, @@ -11625,7 +11802,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } } - if (hasDisplayList) { + if (!useDisplayListProperties && hasDisplayList) { displayList = getDisplayList(); if (!displayList.isValid()) { // Uncommon, but possible. If a view is removed from the hierarchy during the call @@ -11682,7 +11859,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal cachePaint.setDither(false); parent.mCachePaint = cachePaint; } - if (alpha < 1.0f) { + if (alpha < 1) { cachePaint.setAlpha((int) (alpha * 255)); parent.mGroupFlags |= ViewGroup.FLAG_ALPHA_LOWER_THAN_ONE; } else if ((flags & ViewGroup.FLAG_ALPHA_LOWER_THAN_ONE) != 0) { @@ -13481,13 +13658,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal mPrivateFlags |= FORCE_LAYOUT; mPrivateFlags |= INVALIDATED; - if (mParent != null) { - if (mLayoutParams != null) { - mLayoutParams.onResolveLayoutDirection(getResolvedLayoutDirection()); - } - if (!mParent.isLayoutRequested()) { - mParent.requestLayout(); - } + if (mLayoutParams != null) { + mLayoutParams.onResolveLayoutDirection(getResolvedLayoutDirection()); + } + + if (mParent != null && !mParent.isLayoutRequested()) { + mParent.requestLayout(); } } @@ -13982,6 +14158,35 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Returns the current system UI visibility that is currently set for + * the entire window. This is the combination of the + * {@link #setSystemUiVisibility(int)} values supplied by all of the + * views in the window. + */ + public int getWindowSystemUiVisibility() { + return mAttachInfo != null ? mAttachInfo.mSystemUiVisibility : 0; + } + + /** + * Override to find out when the window's requested system UI visibility + * has changed, that is the value returned by {@link #getWindowSystemUiVisibility()}. + * This is different from the callbacks recieved through + * {@link #setOnSystemUiVisibilityChangeListener(OnSystemUiVisibilityChangeListener)} + * in that this is only telling you about the local request of the window, + * not the actual values applied by the system. + */ + public void onWindowSystemUiVisibilityChanged(int visible) { + } + + /** + * Dispatch callbacks to {@link #onWindowSystemUiVisibilityChanged(int)} down + * the view hierarchy. + */ + public void dispatchWindowSystemUiVisiblityChanged(int visible) { + onWindowSystemUiVisibilityChanged(visible); + } + + /** * Set a listener to receive callbacks when the visibility of the system bar changes. * @param l The {@link OnSystemUiVisibilityChangeListener} to receive callbacks. */ diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 30d6ec7..d5c783f 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -918,7 +918,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } } - + + /** + * @hide + */ + @Override + public void makeOptionalFitsSystemWindows() { + super.makeOptionalFitsSystemWindows(); + final int count = mChildrenCount; + final View[] children = mChildren; + for (int i = 0; i < count; i++) { + children[i].makeOptionalFitsSystemWindows(); + } + } + /** * {@inheritDoc} */ @@ -1017,13 +1030,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - void dispatchCollectViewAttributes(int visibility) { - visibility |= mViewFlags&VISIBILITY_MASK; - super.dispatchCollectViewAttributes(visibility); - final int count = mChildrenCount; - final View[] children = mChildren; - for (int i = 0; i < count; i++) { - children[i].dispatchCollectViewAttributes(visibility); + void dispatchCollectViewAttributes(AttachInfo attachInfo, int visibility) { + if ((visibility & VISIBILITY_MASK) == VISIBLE) { + super.dispatchCollectViewAttributes(attachInfo, visibility); + final int count = mChildrenCount; + final View[] children = mChildren; + for (int i = 0; i < count; i++) { + final View child = children[i]; + child.dispatchCollectViewAttributes(attachInfo, + visibility | (child.mViewFlags&VISIBILITY_MASK)); + } } } @@ -1239,6 +1255,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override + public void dispatchWindowSystemUiVisiblityChanged(int visible) { + super.dispatchWindowSystemUiVisiblityChanged(visible); + + final int count = mChildrenCount; + final View[] children = mChildren; + for (int i=0; i <count; i++) { + final View child = children[i]; + child.dispatchWindowSystemUiVisiblityChanged(visible); + } + } + + @Override public void dispatchSystemUiVisibilityChanged(int visible) { super.dispatchSystemUiVisibilityChanged(visible); @@ -2244,12 +2272,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager super.dispatchAttachedToWindow(info, visibility); mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW; - visibility |= mViewFlags & VISIBILITY_MASK; - final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { - children[i].dispatchAttachedToWindow(info, visibility); + final View child = children[i]; + child.dispatchAttachedToWindow(info, + visibility | (child.mViewFlags&VISIBILITY_MASK)); } } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 8395f1b..75e9151 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -271,4 +271,10 @@ public interface ViewParent { * @hide */ public void childHasTransientStateChanged(View child, boolean hasTransientState); + + /** + * Ask that a new dispatch of {@link View#fitSystemWindows(Rect) + * View.fitSystemWindows(Rect)} be performed. + */ + public void requestFitSystemWindows(); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4bd5b94..d72f3b7 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -223,6 +223,7 @@ public final class ViewRootImpl implements ViewParent, long mLastTraversalFinishedTimeNanos; long mLastDrawFinishedTimeNanos; boolean mWillDrawSoon; + boolean mFitSystemWindowsRequested; boolean mLayoutRequested; boolean mFirst; boolean mReportNextDraw; @@ -230,6 +231,7 @@ public final class ViewRootImpl implements ViewParent, boolean mNewSurfaceNeeded; boolean mHasHadWindowFocus; boolean mLastWasImTarget; + int mLastSystemUiVisibility; // Pool of queued input events. private static final int MAX_QUEUED_INPUT_EVENT_POOL_SIZE = 10; @@ -263,6 +265,8 @@ public final class ViewRootImpl implements ViewParent, final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets = new ViewTreeObserver.InternalInsetsInfo(); + final Rect mFitSystemWindowsInsets = new Rect(); + final Configuration mLastConfiguration = new Configuration(); final Configuration mPendingConfiguration = new Configuration(); @@ -539,6 +543,8 @@ public final class ViewRootImpl implements ViewParent, } try { mOrigWindowType = mWindowAttributes.type; + mAttachInfo.mRecomputeGlobalAttributes = true; + collectViewAttributes(); res = sWindowSession.add(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mAttachInfo.mContentInsets, mInputChannel); @@ -786,6 +792,15 @@ public final class ViewRootImpl implements ViewParent, /** * {@inheritDoc} */ + public void requestFitSystemWindows() { + checkThread(); + mFitSystemWindowsRequested = true; + scheduleTraversals(); + } + + /** + * {@inheritDoc} + */ public void requestLayout() { checkThread(); mLayoutRequested = true; @@ -974,6 +989,100 @@ public final class ViewRootImpl implements ViewParent, } } + private boolean collectViewAttributes() { + final View.AttachInfo attachInfo = mAttachInfo; + if (attachInfo.mRecomputeGlobalAttributes) { + //Log.i(TAG, "Computing view hierarchy attributes!"); + attachInfo.mRecomputeGlobalAttributes = false; + boolean oldScreenOn = attachInfo.mKeepScreenOn; + int oldVis = attachInfo.mSystemUiVisibility; + boolean oldHasSystemUiListeners = attachInfo.mHasSystemUiListeners; + attachInfo.mKeepScreenOn = false; + attachInfo.mSystemUiVisibility = 0; + attachInfo.mHasSystemUiListeners = false; + mView.dispatchCollectViewAttributes(attachInfo, 0); + if (attachInfo.mKeepScreenOn != oldScreenOn + || attachInfo.mSystemUiVisibility != oldVis + || attachInfo.mHasSystemUiListeners != oldHasSystemUiListeners) { + WindowManager.LayoutParams params = mWindowAttributes; + if (attachInfo.mKeepScreenOn) { + params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + } + params.subtreeSystemUiVisibility = attachInfo.mSystemUiVisibility; + params.hasSystemUiListeners = attachInfo.mHasSystemUiListeners; + mView.dispatchWindowSystemUiVisiblityChanged(attachInfo.mSystemUiVisibility); + return true; + } + } + return false; + } + + private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, + final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { + int childWidthMeasureSpec; + int childHeightMeasureSpec; + boolean windowSizeMayChange = false; + + if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG, + "Measuring " + host + " in display " + desiredWindowWidth + + "x" + desiredWindowHeight + "..."); + + boolean goodMeasure = false; + if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { + // On large screens, we don't want to allow dialogs to just + // stretch to fill the entire width of the screen to display + // one line of text. First try doing the layout at a smaller + // size to see if it will fit. + final DisplayMetrics packageMetrics = res.getDisplayMetrics(); + res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); + int baseSize = 0; + if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { + baseSize = (int)mTmpValue.getDimension(packageMetrics); + } + if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize); + if (baseSize != 0 && desiredWindowWidth > baseSize) { + childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); + childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); + host.measure(childWidthMeasureSpec, childHeightMeasureSpec); + if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured (" + + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")"); + if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { + goodMeasure = true; + } else { + // Didn't fit in that size... try expanding a bit. + baseSize = (baseSize+desiredWindowWidth)/2; + if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize=" + + baseSize); + childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); + host.measure(childWidthMeasureSpec, childHeightMeasureSpec); + if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured (" + + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")"); + if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { + if (DEBUG_DIALOG) Log.v(TAG, "Good!"); + goodMeasure = true; + } + } + } + } + + if (!goodMeasure) { + childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); + childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); + host.measure(childWidthMeasureSpec, childHeightMeasureSpec); + if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { + windowSizeMayChange = true; + } + } + + if (DBG) { + System.out.println("======================================"); + System.out.println("performTraversals -- after measure"); + host.debug(); + } + + return windowSizeMayChange; + } + private void performTraversals() { // cache mView since it is used so much below... final View host = mView; @@ -995,8 +1104,6 @@ public final class ViewRootImpl implements ViewParent, int desiredWindowWidth; int desiredWindowHeight; - int childWidthMeasureSpec; - int childHeightMeasureSpec; final View.AttachInfo attachInfo = mAttachInfo; @@ -1057,15 +1164,14 @@ public final class ViewRootImpl implements ViewParent, attachInfo.mHasWindowFocus = false; attachInfo.mWindowVisibility = viewVisibility; attachInfo.mRecomputeGlobalAttributes = false; - attachInfo.mKeepScreenOn = false; - attachInfo.mSystemUiVisibility = 0; viewVisibilityChanged = false; mLastConfiguration.setTo(host.getResources().getConfiguration()); + mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility; host.dispatchAttachedToWindow(attachInfo, 0); + mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets); + host.fitSystemWindows(mFitSystemWindowsInsets); //Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn); - host.fitSystemWindows(mAttachInfo.mContentInsets); - } else { desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); @@ -1093,7 +1199,8 @@ public final class ViewRootImpl implements ViewParent, boolean insetsChanged = false; - if (mLayoutRequested && !mStopped) { + boolean layoutRequested = mLayoutRequested && !mStopped; + if (layoutRequested) { // Execute enqueued actions on every layout in case a view that was detached // enqueued an action after being detached getRunQueue().executeActions(attachInfo.mHandler); @@ -1134,79 +1241,12 @@ public final class ViewRootImpl implements ViewParent, } // Ask host how big it wants to be - if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG, - "Measuring " + host + " in display " + desiredWindowWidth - + "x" + desiredWindowHeight + "..."); - - boolean goodMeasure = false; - if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { - // On large screens, we don't want to allow dialogs to just - // stretch to fill the entire width of the screen to display - // one line of text. First try doing the layout at a smaller - // size to see if it will fit. - final DisplayMetrics packageMetrics = res.getDisplayMetrics(); - res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); - int baseSize = 0; - if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { - baseSize = (int)mTmpValue.getDimension(packageMetrics); - } - if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize); - if (baseSize != 0 && desiredWindowWidth > baseSize) { - childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); - childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); - host.measure(childWidthMeasureSpec, childHeightMeasureSpec); - if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured (" - + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")"); - if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { - goodMeasure = true; - } else { - // Didn't fit in that size... try expanding a bit. - baseSize = (baseSize+desiredWindowWidth)/2; - if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize=" - + baseSize); - childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); - host.measure(childWidthMeasureSpec, childHeightMeasureSpec); - if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured (" - + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")"); - if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { - if (DEBUG_DIALOG) Log.v(TAG, "Good!"); - goodMeasure = true; - } - } - } - } - - if (!goodMeasure) { - childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); - childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); - host.measure(childWidthMeasureSpec, childHeightMeasureSpec); - if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { - windowSizeMayChange = true; - } - } - - if (DBG) { - System.out.println("======================================"); - System.out.println("performTraversals -- after measure"); - host.debug(); - } + windowSizeMayChange |= measureHierarchy(host, lp, res, + desiredWindowWidth, desiredWindowHeight); } - if (attachInfo.mRecomputeGlobalAttributes && host.mAttachInfo != null) { - //Log.i(TAG, "Computing view hierarchy attributes!"); - attachInfo.mRecomputeGlobalAttributes = false; - boolean oldScreenOn = attachInfo.mKeepScreenOn; - int oldVis = attachInfo.mSystemUiVisibility; - boolean oldHasSystemUiListeners = attachInfo.mHasSystemUiListeners; - attachInfo.mKeepScreenOn = false; - attachInfo.mSystemUiVisibility = 0; - attachInfo.mHasSystemUiListeners = false; - host.dispatchCollectViewAttributes(0); - if (attachInfo.mKeepScreenOn != oldScreenOn - || attachInfo.mSystemUiVisibility != oldVis - || attachInfo.mHasSystemUiListeners != oldHasSystemUiListeners) { - params = lp; - } + if (collectViewAttributes()) { + params = lp; } if (attachInfo.mForceReportNewAttributes) { attachInfo.mForceReportNewAttributes = false; @@ -1245,7 +1285,28 @@ public final class ViewRootImpl implements ViewParent, } } - boolean windowShouldResize = mLayoutRequested && windowSizeMayChange + if (mFitSystemWindowsRequested) { + mFitSystemWindowsRequested = false; + mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets); + host.fitSystemWindows(mFitSystemWindowsInsets); + if (mLayoutRequested) { + // Short-circuit catching a new layout request here, so + // we don't need to go through two layout passes when things + // change due to fitting system windows, which can happen a lot. + windowSizeMayChange |= measureHierarchy(host, lp, + mView.getContext().getResources(), + desiredWindowWidth, desiredWindowHeight); + } + } + + if (layoutRequested) { + // Clear this now, so that if anything requests a layout in the + // rest of this function we will catch it and re-run a full + // layout pass. + mLayoutRequested = false; + } + + boolean windowShouldResize = layoutRequested && windowSizeMayChange && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() != mWidth) @@ -1285,15 +1346,6 @@ public final class ViewRootImpl implements ViewParent, boolean hadSurface = mSurface.isValid(); try { - int fl = 0; - if (params != null) { - fl = params.flags; - if (attachInfo.mKeepScreenOn) { - params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - } - params.subtreeSystemUiVisibility = attachInfo.mSystemUiVisibility; - params.hasSystemUiListeners = attachInfo.mHasSystemUiListeners; - } if (DEBUG_LAYOUT) { Log.i(TAG, "host=w:" + host.getMeasuredWidth() + ", h:" + host.getMeasuredHeight() + ", params=" + params); @@ -1302,10 +1354,6 @@ public final class ViewRootImpl implements ViewParent, final int surfaceGenerationId = mSurface.getGenerationId(); relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); - if (params != null) { - params.flags = fl; - } - if (DEBUG_LAYOUT) Log.v(TAG, "relayout: frame=" + frame.toShortString() + " content=" + mPendingContentInsets.toShortString() + " visible=" + mPendingVisibleInsets.toShortString() @@ -1323,7 +1371,9 @@ public final class ViewRootImpl implements ViewParent, visibleInsetsChanged = !mPendingVisibleInsets.equals( mAttachInfo.mVisibleInsets); if (contentInsetsChanged) { - if (mWidth > 0 && mHeight > 0 && + if (mWidth > 0 && mHeight > 0 && lp != null && + ((lp.systemUiVisibility|lp.subtreeSystemUiVisibility) + & View.SYSTEM_UI_LAYOUT_FLAGS) == 0 && mSurface != null && mSurface.isValid() && !mAttachInfo.mTurnOffWindowResizeAnim && mAttachInfo.mHardwareRenderer != null && @@ -1390,10 +1440,16 @@ public final class ViewRootImpl implements ViewParent, } } mAttachInfo.mContentInsets.set(mPendingContentInsets); - host.fitSystemWindows(mAttachInfo.mContentInsets); if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: " + mAttachInfo.mContentInsets); } + if (contentInsetsChanged || mLastSystemUiVisibility != + mAttachInfo.mSystemUiVisibility || mFitSystemWindowsRequested) { + mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility; + mFitSystemWindowsRequested = false; + mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets); + host.fitSystemWindows(mFitSystemWindowsInsets); + } if (visibleInsetsChanged) { mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: " @@ -1547,8 +1603,8 @@ public final class ViewRootImpl implements ViewParent, (relayoutResult&WindowManagerImpl.RELAYOUT_RES_IN_TOUCH_MODE) != 0); if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged) { - childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); - childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); + int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); + int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth=" + mWidth + " measuredWidth=" + host.getMeasuredWidth() @@ -1586,12 +1642,12 @@ public final class ViewRootImpl implements ViewParent, host.measure(childWidthMeasureSpec, childHeightMeasureSpec); } - mLayoutRequested = true; + layoutRequested = true; } } } - final boolean didLayout = mLayoutRequested && !mStopped; + final boolean didLayout = layoutRequested && !mStopped; boolean triggerGlobalLayoutListener = didLayout || attachInfo.mRecomputeGlobalAttributes; if (didLayout) { @@ -3579,9 +3635,6 @@ public final class ViewRootImpl implements ViewParent, if (mView == null) return; if (args.localChanges != 0) { if (mAttachInfo != null) { - mAttachInfo.mSystemUiVisibility = - (mAttachInfo.mSystemUiVisibility & ~args.localChanges) | - (args.localValue & args.localChanges); mAttachInfo.mRecomputeGlobalAttributes = true; } mView.updateLocalSystemUiVisibility(args.localValue, args.localChanges); @@ -4056,9 +4109,13 @@ public final class ViewRootImpl implements ViewParent, mChoreographer.removeCallbacks(Choreographer.CALLBACK_INPUT, mConsumedBatchedInputRunnable, null); } - if (mInputEventReceiver != null) { - mInputEventReceiver.consumeBatchedInputEvents(); - } + } + + // Always consume batched input events even if not scheduled, because there + // might be new input there waiting for us that we have no noticed yet because + // the Looper has not had a chance to run again. + if (mInputEventReceiver != null) { + mInputEventReceiver.consumeBatchedInputEvents(); } } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index a99ac03..b0e90db 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -75,6 +75,16 @@ public abstract class Window { * over how the Action Bar is displayed, such as letting application content scroll beneath * an Action Bar with a transparent background or otherwise displaying a transparent/translucent * Action Bar over application content. + * + * <p>This mode is especially useful with {@link View#SYSTEM_UI_FLAG_FULLSCREEN + * View.SYSTEM_UI_FLAG_FULLSCREEN}, which allows you to seamlessly hide the + * action bar in conjunction with other screen decorations. + * + * <p>As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, when an + * ActionBar is in this mode it will adjust the insets provided to + * {@link View#fitSystemWindows(android.graphics.Rect) View.fitSystemWindows(Rect)} + * to include the content covered by the action bar, so you can do layout within + * that space. */ public static final int FEATURE_ACTION_BAR_OVERLAY = 9; /** diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index f2ee9f9..a45a87e 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -510,8 +510,13 @@ public class WindowManagerImpl implements WindowManager { String name = root.getClass().getName() + '@' + Integer.toHexString(hashCode()); - pw.printf(" %s: %d views, %.2f kB (display lists)\n", + pw.printf(" %s: %d views, %.2f kB (display lists)", name, info[0], info[1] / 1024.0f); + HardwareRenderer renderer = root.getView().mAttachInfo.mHardwareRenderer; + if (renderer != null) { + pw.printf(", %d frames rendered", renderer.getFrameCount()); + } + pw.printf("\n"); viewsCount += info[0]; displayListsSize += info[1]; diff --git a/core/java/android/webkit/WebSettingsClassic.java b/core/java/android/webkit/WebSettingsClassic.java index c41bc00..94b46fc 100644 --- a/core/java/android/webkit/WebSettingsClassic.java +++ b/core/java/android/webkit/WebSettingsClassic.java @@ -90,6 +90,7 @@ public class WebSettingsClassic extends WebSettings { private boolean mWorkersEnabled = false; // only affects V8. private boolean mGeolocationEnabled = true; private boolean mXSSAuditorEnabled = false; + private boolean mLinkPrefetchEnabled = false; // HTML5 configuration parameters private long mAppCacheMaxSize = Long.MAX_VALUE; private String mAppCachePath = null; @@ -1305,6 +1306,16 @@ public class WebSettingsClassic extends WebSettings { } /** + * Enables/disables HTML5 link "prefetch" parameter. + */ + public synchronized void setLinkPrefetchEnabled(boolean flag) { + if (mLinkPrefetchEnabled != flag) { + mLinkPrefetchEnabled = flag; + postSync(); + } + } + + /** * @see android.webkit.WebSettings#getJavaScriptEnabled() */ @Override diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index d225594..422b48d 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -1883,12 +1883,14 @@ public class WebView extends AbsoluteLayout @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(WebView.class.getName()); mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(WebView.class.getName()); mProvider.getViewDelegate().onInitializeAccessibilityEvent(event); } diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index 5a0818b..45c5fa0 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -1204,10 +1204,8 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc static final int SHOW_FULLSCREEN = 120; static final int HIDE_FULLSCREEN = 121; static final int REPLACE_BASE_CONTENT = 123; - static final int FORM_DID_BLUR = 124; static final int UPDATE_MATCH_COUNT = 126; static final int CENTER_FIT_RECT = 127; - static final int REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID = 128; static final int SET_SCROLLBAR_MODES = 129; static final int SELECTION_STRING_CHANGED = 130; static final int HIT_TEST_RESULT = 131; @@ -1274,7 +1272,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc "HIDE_FULLSCREEN", // = 121; "DOM_FOCUS_CHANGED", // = 122; "REPLACE_BASE_CONTENT", // = 123; - "FORM_DID_BLUR", // = 124; "RETURN_LABEL", // = 125; "UPDATE_MATCH_COUNT", // = 126; "CENTER_FIT_RECT", // = 127; @@ -1420,6 +1417,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // Used to notify listeners about find-on-page results. private WebView.FindListener mFindListener; + // Used to prevent resending save password message + private Message mResumeMsg; + /** * Refer to {@link WebView#requestFocusNodeHref(Message)} for more information */ @@ -1868,11 +1868,17 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc /* package */ boolean onSavePassword(String schemePlusHost, String username, String password, final Message resumeMsg) { - boolean rVal = false; - if (resumeMsg == null) { - // null resumeMsg implies saving password silently - mDatabase.setUsernamePassword(schemePlusHost, username, password); - } else { + boolean rVal = false; + if (resumeMsg == null) { + // null resumeMsg implies saving password silently + mDatabase.setUsernamePassword(schemePlusHost, username, password); + } else { + if (mResumeMsg != null) { + Log.w(LOGTAG, "onSavePassword should not be called while dialog is up"); + resumeMsg.sendToTarget(); + return true; + } + mResumeMsg = resumeMsg; final Message remember = mPrivateHandler.obtainMessage( REMEMBER_PASSWORD); remember.getData().putString("host", schemePlusHost); @@ -1894,34 +1900,46 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - resumeMsg.sendToTarget(); + if (mResumeMsg != null) { + resumeMsg.sendToTarget(); + mResumeMsg = null; + } } }) .setNeutralButton(com.android.internal.R.string.save_password_remember, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - remember.sendToTarget(); + if (mResumeMsg != null) { + remember.sendToTarget(); + mResumeMsg = null; + } } }) .setNegativeButton(com.android.internal.R.string.save_password_never, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - neverRemember.sendToTarget(); + if (mResumeMsg != null) { + neverRemember.sendToTarget(); + mResumeMsg = null; + } } }) .setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { - resumeMsg.sendToTarget(); + if (mResumeMsg != null) { + resumeMsg.sendToTarget(); + mResumeMsg = null; + } } }).show(); // Return true so that WebViewCore will pause while the dialog is // up. rVal = true; } - return rVal; + return rVal; } @Override @@ -3877,7 +3895,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc private void scrollLayerTo(int x, int y) { int dx = mScrollingLayerRect.left - x; int dy = mScrollingLayerRect.top - y; - if (dx == 0 && y == 0) { + if (dx == 0 && dy == 0) { return; } if (mSelectingText) { @@ -8267,16 +8285,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc } } break; - case REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID: - displaySoftKeyboard(true); - // fall through to UPDATE_TEXT_SELECTION_MSG_ID case UPDATE_TEXT_SELECTION_MSG_ID: updateTextSelectionFromMessage(msg.arg1, msg.arg2, (WebViewCore.TextSelectionData) msg.obj); break; - case FORM_DID_BLUR: - // TODO: Figure out if this is needed for something (b/6111763) - break; case TAKE_FOCUS: int direction = msg.arg1; View focusSearch = mWebView.focusSearch(direction); diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 3eba6d7..b4ebc09 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -325,17 +325,6 @@ public final class WebViewCore { } /** - * Called by JNI. Send a message to the UI thread to hide the soft keyboard - * if the node pointed to by nodePointer is still in focus. - * @param nodePointer The node which just blurred. - */ - private void formDidBlur(int nodePointer) { - if (mWebViewClassic == null) return; - Message.obtain(mWebViewClassic.mPrivateHandler, WebViewClassic.FORM_DID_BLUR, - nodePointer, 0).sendToTarget(); - } - - /** * Called by JNI when the focus node changed. */ private void focusNodeChanged(int nodePointer, WebKitHitTest hitTest) { @@ -2830,7 +2819,7 @@ public final class WebViewCore { Message.obtain(mWebViewClassic.mPrivateHandler, WebViewClassic.INIT_EDIT_FIELD, initData).sendToTarget(); Message.obtain(mWebViewClassic.mPrivateHandler, - WebViewClassic.REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID, + WebViewClassic.UPDATE_TEXT_SELECTION_MSG_ID, initData.mFieldPointer, 0, new TextSelectionData(start, end, selectionPtr)) .sendToTarget(); 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/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 586fdf4..8067435 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -477,8 +477,11 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback private static final String TAG = "FixedSizeRemoteViewsCache"; // The meta data related to all the RemoteViews, ie. count, is stable, etc. - private RemoteViewsMetaData mMetaData; - private RemoteViewsMetaData mTemporaryMetaData; + // The meta data objects are made final so that they can be locked on independently + // of the FixedSizeRemoteViewsCache. If we ever lock on both meta data objects, it is in + // the order mTemporaryMetaData followed by mMetaData. + private final RemoteViewsMetaData mMetaData; + private final RemoteViewsMetaData mTemporaryMetaData; // The cache/mapping of position to RemoteViewsMetaData. This set is guaranteed to be // greater than or equal to the set of RemoteViews. @@ -939,6 +942,10 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback * which wouldn't otherwise be possible. */ public void setVisibleRangeHint(int lowerBound, int upperBound) { + if (lowerBound < 0 || upperBound < 0) { + throw new RuntimeException("Attempted to set invalid range: lowerBound="+lowerBound + + "," + "upperBound="+upperBound); + } mVisibleWindowLowerBound = lowerBound; mVisibleWindowUpperBound = upperBound; } @@ -1072,12 +1079,20 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // Re-request the new metadata (only after the notification to the factory) updateTemporaryMetaData(); + int newCount; + synchronized(mCache.getTemporaryMetaData()) { + newCount = mCache.getTemporaryMetaData().count; + } // Pre-load (our best guess of) the views which are currently visible in the AdapterView. // This mitigates flashing and flickering of loading views when a widget notifies that // its data has changed. for (int i = mVisibleWindowLowerBound; i <= mVisibleWindowUpperBound; i++) { - updateRemoteViews(i, false, false); + // Because temporary meta data is only ever modified from this thread (ie. + // mWorkerThread), it is safe to assume that count is a valid representation. + if (i < newCount) { + updateRemoteViews(i, false, false); + } } // Propagate the notification back to the base adapter 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 @@ package android.widget; import android.R; import android.content.ClipData; -import android.content.ClipData.Item; import android.content.ClipboardManager; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.CompatibilityInfo; import android.content.res.Resources; @@ -43,7 +40,6 @@ import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; -import android.provider.Settings; import android.text.BoringLayout; import android.text.DynamicLayout; import android.text.Editable; @@ -57,7 +53,6 @@ import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableString; -import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; @@ -86,42 +81,31 @@ import android.text.method.TransformationMethod2; import android.text.method.WordIterator; import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; -import android.text.style.EasyEditSpan; import android.text.style.ParagraphStyle; import android.text.style.SpellCheckSpan; -import android.text.style.SuggestionRangeSpan; import android.text.style.SuggestionSpan; -import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.util.FloatMath; import android.util.Log; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ActionMode.Callback; -import android.view.DisplayList; import android.view.DragEvent; import android.view.Gravity; import android.view.HapticFeedbackConstants; -import android.view.HardwareCanvas; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewDebug; -import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; -import android.view.ViewParent; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; -import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; @@ -136,10 +120,8 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.textservice.SpellCheckerSubtype; import android.view.textservice.TextServicesManager; -import android.widget.AdapterView.OnItemClickListener; import android.widget.RemoteViews.RemoteView; -import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastMath; import com.android.internal.widget.EditableInputConnection; @@ -147,11 +129,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.lang.ref.WeakReference; -import java.text.BreakIterator; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; import java.util.Locale; /** @@ -267,24 +245,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private static final int PIXELS = 2; private static final RectF TEMP_RECTF = new RectF(); - private static final float[] TEMP_POSITION = new float[2]; // XXX should be much larger private static final int VERY_WIDE = 1024*1024; - private static final int BLINK = 500; private static final int ANIMATED_SCROLL_GAP = 250; private static final InputFilter[] NO_FILTERS = new InputFilter[0]; private static final Spanned EMPTY_SPANNED = new SpannedString(""); - private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; private static final int CHANGE_WATCHER_PRIORITY = 100; // New state used to change background based on whether this TextView is multiline. private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline }; // System wide time for last cut or copy action. - private static long LAST_CUT_OR_COPY_TIME; + static long LAST_CUT_OR_COPY_TIME; private int mCurrentAlpha = 255; @@ -316,7 +291,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mDrawableHeightStart, mDrawableHeightEnd; int mDrawablePadding; } - private Drawables mDrawables; + Drawables mDrawables; private CharWrapper mCharWrapper; @@ -404,23 +379,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // It is possible to have a selection even when mEditor is null (programmatically set, like when // a link is pressed). These highlight-related fields do not go in mEditor. - private int mHighlightColor = 0x6633B5E5; + int mHighlightColor = 0x6633B5E5; private Path mHighlightPath; private final Paint mHighlightPaint; private boolean mHighlightPathBogus = true; // Although these fields are specific to editable text, they are not added to Editor because // they are defined by the TextView's style and are theme-dependent. - private int mCursorDrawableRes; + int mCursorDrawableRes; // These four fields, could be moved to Editor, since we know their default values and we // could condition the creation of the Editor to a non standard value. This is however // brittle since the hardcoded values here (such as // com.android.internal.R.drawable.text_select_handle_left) would have to be updated if the // default style is modified. - private int mTextSelectHandleLeftRes; - private int mTextSelectHandleRightRes; - private int mTextSelectHandleRes; - private int mTextEditSuggestionItemLayout; + int mTextSelectHandleLeftRes; + int mTextSelectHandleRightRes; + int mTextSelectHandleRes; + int mTextEditSuggestionItemLayout; /** * EditText specific data, created on demand when one of the Editor fields is used. @@ -826,26 +801,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_imeOptions: createEditorIfNeeded("IME options specified in constructor"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeOptions = a.getInt(attr, getEditor().mInputContentType.imeOptions); break; case com.android.internal.R.styleable.TextView_imeActionLabel: createEditorIfNeeded("IME action label specified in constructor"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeActionLabel = a.getText(attr); break; case com.android.internal.R.styleable.TextView_imeActionId: createEditorIfNeeded("IME action id specified in constructor"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeActionId = a.getInt(attr, getEditor().mInputContentType.imeActionId); break; @@ -1135,7 +1104,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setClickable(clickable); setLongClickable(longClickable); - prepareCursorControllers(); + if (mEditor != null) mEditor.prepareCursorControllers(); } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { @@ -1216,11 +1185,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Will change text color - if (mEditor != null) getEditor().invalidateTextDisplayList(); - prepareCursorControllers(); + if (mEditor != null) { + getEditor().invalidateTextDisplayList(); + getEditor().prepareCursorControllers(); - // start or stop the cursor blinking as appropriate - makeBlink(); + // start or stop the cursor blinking as appropriate + getEditor().makeBlink(); + } } /** @@ -1412,7 +1383,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener fixFocusableAndClickableSettings(); // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } } @@ -3250,7 +3221,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // SelectionModifierCursorController depends on textCanBeSelected, which depends on text - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } /** @@ -3366,12 +3337,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mHint; } + boolean isSingleLine() { + return mSingleLine; + } + private static boolean isMultilineInputType(int type) { return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); } /** + * Removes the suggestion spans. + */ + CharSequence removeSuggestionSpans(CharSequence text) { + if (text instanceof Spanned) { + Spannable spannable; + if (text instanceof Spannable) { + spannable = (Spannable) text; + } else { + spannable = new SpannableString(text); + text = spannable; + } + + SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class); + for (int i = 0; i < spans.length; i++) { + spannable.removeSpan(spans[i]); + } + } + return text; + } + + /** * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)}, * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL} @@ -3543,9 +3539,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setImeOptions(int imeOptions) { createEditorIfNeeded("IME options specified"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeOptions = imeOptions; } @@ -3572,9 +3566,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setImeActionLabel(CharSequence label, int actionId) { createEditorIfNeeded("IME action label specified"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.imeActionLabel = label; getEditor().mInputContentType.imeActionId = actionId; } @@ -3611,9 +3603,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setOnEditorActionListener(OnEditorActionListener l) { createEditorIfNeeded("Editor action listener set"); - if (getEditor().mInputContentType == null) { - getEditor().mInputContentType = new InputContentType(); - } + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.onEditorActionListener = l; } @@ -3638,7 +3628,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @see #setOnEditorActionListener */ public void onEditorAction(int actionCode) { - final InputContentType ict = mEditor == null ? null : getEditor().mInputContentType; + final Editor.InputContentType ict = mEditor == null ? null : getEditor().mInputContentType; if (ict != null) { if (ict.onEditorActionListener != null) { if (ict.onEditorActionListener.onEditorAction(this, @@ -3710,8 +3700,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setPrivateImeOptions(String type) { createEditorIfNeeded("Private IME option set"); - if (getEditor().mInputContentType == null) - getEditor().mInputContentType = new InputContentType(); + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.privateImeOptions = type; } @@ -3740,8 +3729,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void setInputExtras(int xmlResId) throws XmlPullParserException, IOException { createEditorIfNeeded("Input extra set"); XmlResourceParser parser = getResources().getXml(xmlResId); - if (getEditor().mInputContentType == null) - getEditor().mInputContentType = new InputContentType(); + getEditor().createInputContentTypeIfNeeded(); getEditor().mInputContentType.extras = new Bundle(); getResources().parseBundleExtras(parser, getEditor().mInputContentType.extras); } @@ -3761,7 +3749,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener createEditorIfNeeded("get Input extra"); if (getEditor().mInputContentType == null) { if (!create) return null; - getEditor().mInputContentType = new InputContentType(); + getEditor().createInputContentTypeIfNeeded(); } if (getEditor().mInputContentType.extras == null) { if (!create) return null; @@ -3811,142 +3799,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setError(CharSequence error, Drawable icon) { createEditorIfNeeded("setError"); - error = TextUtils.stringOrSpannedString(error); - - getEditor().mError = error; - getEditor().mErrorWasChanged = true; - final Drawables dr = mDrawables; - if (dr != null) { - switch (getResolvedLayoutDirection()) { - default: - case LAYOUT_DIRECTION_LTR: - setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, - dr.mDrawableBottom); - break; - case LAYOUT_DIRECTION_RTL: - setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight, - dr.mDrawableBottom); - break; - } - } else { - setCompoundDrawables(null, null, icon, null); - } - - if (error == null) { - if (getEditor().mErrorPopup != null) { - if (getEditor().mErrorPopup.isShowing()) { - getEditor().mErrorPopup.dismiss(); - } - - getEditor().mErrorPopup = null; - } - } else { - if (isFocused()) { - showError(); - } - } - } - - private void showError() { - if (getWindowToken() == null) { - getEditor().mShowErrorAfterAttach = true; - return; - } - - if (getEditor().mErrorPopup == null) { - LayoutInflater inflater = LayoutInflater.from(getContext()); - final TextView err = (TextView) inflater.inflate( - com.android.internal.R.layout.textview_hint, null); - - final float scale = getResources().getDisplayMetrics().density; - getEditor().mErrorPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f)); - getEditor().mErrorPopup.setFocusable(false); - // The user is entering text, so the input method is needed. We - // don't want the popup to be displayed on top of it. - getEditor().mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - } - - TextView tv = (TextView) getEditor().mErrorPopup.getContentView(); - chooseSize(getEditor().mErrorPopup, getEditor().mError, tv); - tv.setText(getEditor().mError); - - getEditor().mErrorPopup.showAsDropDown(this, getErrorX(), getErrorY()); - getEditor().mErrorPopup.fixDirection(getEditor().mErrorPopup.isAboveAnchor()); - } - - /** - * Returns the Y offset to make the pointy top of the error point - * at the middle of the error icon. - */ - private int getErrorX() { - /* - * The "25" is the distance between the point and the right edge - * of the background - */ - final float scale = getResources().getDisplayMetrics().density; - - final Drawables dr = mDrawables; - return getWidth() - getEditor().mErrorPopup.getWidth() - getPaddingRight() - - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); - } - - /** - * Returns the Y offset to make the pointy top of the error point - * at the bottom of the error icon. - */ - private int getErrorY() { - /* - * Compound, not extended, because the icon is not clipped - * if the text height is smaller. - */ - final int compoundPaddingTop = getCompoundPaddingTop(); - int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop; - - final Drawables dr = mDrawables; - int icontop = compoundPaddingTop + - (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; - - /* - * The "2" is the distance between the point and the top edge - * of the background. - */ - final float scale = getResources().getDisplayMetrics().density; - return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() - - (int) (2 * scale + 0.5f); - } - - private void hideError() { - if (getEditor().mErrorPopup != null) { - if (getEditor().mErrorPopup.isShowing()) { - getEditor().mErrorPopup.dismiss(); - } - } - - getEditor().mShowErrorAfterAttach = false; - } - - private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { - int wid = tv.getPaddingLeft() + tv.getPaddingRight(); - int ht = tv.getPaddingTop() + tv.getPaddingBottom(); - - int defaultWidthInPixels = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.textview_error_popup_default_width); - Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels, - Layout.Alignment.ALIGN_NORMAL, 1, 0, true); - float max = 0; - for (int i = 0; i < l.getLineCount(); i++) { - max = Math.max(max, l.getLineWidth(i)); - } - - /* - * Now set the popup size to be big enough for the text plus the border capped - * to DEFAULT_MAX_POPUP_WIDTH - */ - pop.setWidth(wid + (int) Math.ceil(max)); - pop.setHeight(ht + l.getHeight()); + getEditor().setError(error, icon); } - @Override protected boolean setFrame(int l, int t, int r, int b) { boolean result = super.setFrame(l, t, r, b); @@ -4009,7 +3864,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ///////////////////////////////////////////////////////////////////////// - private int getVerticalOffset(boolean forceNormal) { + int getVerticalOffset(boolean forceNormal) { int voffset = 0; final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; @@ -4071,7 +3926,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return voffset; } - private void invalidateCursorPath() { + void invalidateCursorPath() { if (mHighlightPathBogus) { invalidateCursor(); } else { @@ -4114,7 +3969,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void invalidateCursor() { + void invalidateCursor() { int where = getSelectionEnd(); invalidateCursor(where, where, where); @@ -4130,8 +3985,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Invalidates the region of text enclosed between the start and end text offsets. - * - * @hide */ void invalidateRegion(int start, int end, boolean invalidateCursor) { if (mLayout == null) { @@ -4237,15 +4090,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // - onFocusChanged cannot start it when focus is given to a view with selected text (after // a screen rotation) since layout is not yet initialized at that point. if (mEditor != null && getEditor().mCreatedWithASelection) { - startSelectionActionMode(); + getEditor().startSelectionActionMode(); getEditor().mCreatedWithASelection = false; } // Phone specific code (there is no ExtractEditText on tablets). // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can // not be set. Do the test here instead. - if (this instanceof ExtractEditText && hasSelection()) { - startSelectionActionMode(); + if (this instanceof ExtractEditText && hasSelection() && mEditor != null) { + getEditor().startSelectionActionMode(); } getViewTreeObserver().removeOnPreDrawListener(this); @@ -4260,11 +4113,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTemporaryDetach = false; - if (mEditor != null && getEditor().mShowErrorAfterAttach) { - showError(); - getEditor().mShowErrorAfterAttach = false; - } - // Resolve drawables as the layout direction has been resolved resolveDrawables(); @@ -4495,7 +4343,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL); // Called by setText above, but safer in case of future code changes - prepareCursorControllers(); + getEditor().prepareCursorControllers(); } @Override @@ -4536,8 +4384,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int selEnd = getSelectionEnd(); if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) { if (selStart == selEnd) { - if (mEditor != null && isCursorVisible() && - (SystemClock.uptimeMillis() - getEditor().mShowCursor) % (2 * BLINK) < BLINK) { + if (mEditor != null && getEditor().isCursorVisible() && + (SystemClock.uptimeMillis() - getEditor().mShowCursor) % + (2 * Editor.BLINK) < Editor.BLINK) { if (mHighlightPathBogus) { if (mHighlightPath == null) mHighlightPath = new Path(); mHighlightPath.reset(); @@ -4730,14 +4579,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Path highlight = getUpdatedHighlightPath(); if (mEditor != null) { - getEditor().onDraw(canvas, layout, highlight, cursorOffsetVertical); + getEditor().onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical); } else { layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); + } - if (mMarquee != null && mMarquee.shouldDrawGhost()) { - canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); - layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); - } + if (mMarquee != null && mMarquee.shouldDrawGhost()) { + canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); + layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); } canvas.restore(); @@ -4853,7 +4702,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @hide - * @param offsetRequired */ @Override protected int getFadeTop(boolean offsetRequired) { @@ -4871,7 +4719,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @hide - * @param offsetRequired */ @Override protected int getFadeHeight(boolean offsetRequired) { @@ -5252,9 +5099,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (onCheckIsTextEditor() && isEnabled()) { - if (getEditor().mInputMethodState == null) { - getEditor().mInputMethodState = new InputMethodState(); - } + getEditor().createInputMethodStateIfNeeded(); outAttrs.inputType = getInputType(); if (getEditor().mInputContentType != null) { outAttrs.imeOptions = getEditor().mInputContentType.imeOptions; @@ -5307,122 +5152,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * based on the information in <var>request</var> in to <var>outText</var>. * @return Returns true if the text was successfully extracted, else false. */ - public boolean extractText(ExtractedTextRequest request, - ExtractedText outText) { - return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, - EXTRACT_UNKNOWN, outText); + public boolean extractText(ExtractedTextRequest request, ExtractedText outText) { + createEditorIfNeeded("extractText"); + return getEditor().extractText(request, outText); } - - static final int EXTRACT_NOTHING = -2; - static final int EXTRACT_UNKNOWN = -1; - - boolean extractTextInternal(ExtractedTextRequest request, - int partialStartOffset, int partialEndOffset, int delta, - ExtractedText outText) { - final CharSequence content = mText; - if (content != null) { - if (partialStartOffset != EXTRACT_NOTHING) { - final int N = content.length(); - if (partialStartOffset < 0) { - outText.partialStartOffset = outText.partialEndOffset = -1; - partialStartOffset = 0; - partialEndOffset = N; - } else { - // Now use the delta to determine the actual amount of text - // we need. - partialEndOffset += delta; - // Adjust offsets to ensure we contain full spans. - if (content instanceof Spanned) { - Spanned spanned = (Spanned)content; - Object[] spans = spanned.getSpans(partialStartOffset, - partialEndOffset, ParcelableSpan.class); - int i = spans.length; - while (i > 0) { - i--; - int j = spanned.getSpanStart(spans[i]); - if (j < partialStartOffset) partialStartOffset = j; - j = spanned.getSpanEnd(spans[i]); - if (j > partialEndOffset) partialEndOffset = j; - } - } - outText.partialStartOffset = partialStartOffset; - outText.partialEndOffset = partialEndOffset - delta; - if (partialStartOffset > N) { - partialStartOffset = N; - } else if (partialStartOffset < 0) { - partialStartOffset = 0; - } - if (partialEndOffset > N) { - partialEndOffset = N; - } else if (partialEndOffset < 0) { - partialEndOffset = 0; - } - } - if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { - outText.text = content.subSequence(partialStartOffset, - partialEndOffset); - } else { - outText.text = TextUtils.substring(content, partialStartOffset, - partialEndOffset); - } - } else { - outText.partialStartOffset = 0; - outText.partialEndOffset = 0; - outText.text = ""; - } - outText.flags = 0; - if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { - outText.flags |= ExtractedText.FLAG_SELECTING; - } - if (mSingleLine) { - outText.flags |= ExtractedText.FLAG_SINGLE_LINE; - } - outText.startOffset = 0; - outText.selectionStart = getSelectionStart(); - outText.selectionEnd = getSelectionEnd(); - return true; - } - return false; - } - - boolean reportExtractedText() { - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null) { - final boolean contentChanged = ims.mContentChanged; - if (contentChanged || ims.mSelectionModeChanged) { - ims.mContentChanged = false; - ims.mSelectionModeChanged = false; - final ExtractedTextRequest req = ims.mExtracting; - if (req != null) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start=" - + ims.mChangedStart + " end=" + ims.mChangedEnd - + " delta=" + ims.mChangedDelta); - if (ims.mChangedStart < 0 && !contentChanged) { - ims.mChangedStart = EXTRACT_NOTHING; - } - if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, - ims.mChangedDelta, ims.mTmpExtracted)) { - if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start=" - + ims.mTmpExtracted.partialStartOffset - + " end=" + ims.mTmpExtracted.partialEndOffset - + ": " + ims.mTmpExtracted.text); - imm.updateExtractedText(this, req.token, ims.mTmpExtracted); - ims.mChangedStart = EXTRACT_UNKNOWN; - ims.mChangedEnd = EXTRACT_UNKNOWN; - ims.mChangedDelta = 0; - ims.mContentChanged = false; - return true; - } - } - } - } - } - return false; - } - /** * This is used to remove all style-impacting spans from text before new * extracted text is being replaced into it, so that we don't have any @@ -5436,7 +5170,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener spannable.removeSpan(spans[i]); } } - + /** * Apply to this text view the given extracted text, as previously * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}. @@ -5492,7 +5226,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // This would stop a possible selection mode, but no such mode is started in case // extracted mode will start. Some text is selected though, and will trigger an action mode // in the extracted view. - hideControllers(); + getEditor().hideControllers(); } /** @@ -5518,87 +5252,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @param info The auto correct info about the text that was corrected. */ public void onCommitCorrection(CorrectionInfo info) { - if (mEditor == null) return; - if (getEditor().mCorrectionHighlighter == null) { - getEditor().mCorrectionHighlighter = new CorrectionHighlighter(); - } else { - getEditor().mCorrectionHighlighter.invalidate(false); - } - - getEditor().mCorrectionHighlighter.highlight(info); + if (mEditor != null) getEditor().onCommitCorrection(info); } public void beginBatchEdit() { - if (mEditor == null) return; - getEditor().mInBatchEditControllers = true; - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null) { - int nesting = ++ims.mBatchEditNesting; - if (nesting == 1) { - ims.mCursorChanged = false; - ims.mChangedDelta = 0; - if (ims.mContentChanged) { - // We already have a pending change from somewhere else, - // so turn this into a full update. - ims.mChangedStart = 0; - ims.mChangedEnd = mText.length(); - } else { - ims.mChangedStart = EXTRACT_UNKNOWN; - ims.mChangedEnd = EXTRACT_UNKNOWN; - ims.mContentChanged = false; - } - onBeginBatchEdit(); - } - } + if (mEditor != null) getEditor().beginBatchEdit(); } public void endBatchEdit() { - if (mEditor == null) return; - getEditor().mInBatchEditControllers = false; - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null) { - int nesting = --ims.mBatchEditNesting; - if (nesting == 0) { - finishBatchEdit(ims); - } - } - } - - void ensureEndedBatchEdit() { - final InputMethodState ims = getEditor().mInputMethodState; - if (ims != null && ims.mBatchEditNesting != 0) { - ims.mBatchEditNesting = 0; - finishBatchEdit(ims); - } - } - - void finishBatchEdit(final InputMethodState ims) { - onEndBatchEdit(); - - if (ims.mContentChanged || ims.mSelectionModeChanged) { - updateAfterEdit(); - reportExtractedText(); - } else if (ims.mCursorChanged) { - // Cheezy way to get us to report the current cursor location. - invalidateCursor(); - } - } - - void updateAfterEdit() { - invalidate(); - int curs = getSelectionStart(); - - if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { - registerForPreDraw(); - } - - if (curs >= 0) { - mHighlightPathBogus = true; - makeBlink(); - bringPointIntoView(curs); - } - - checkForResize(); + if (mEditor != null) getEditor().endBatchEdit(); } /** @@ -5644,7 +5306,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mBoring = mHintBoring = null; // Since it depends on the value of mLayout - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } /** @@ -5826,7 +5488,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // CursorControllers need a non-null mLayout - prepareCursorControllers(); + if (mEditor != null) getEditor().prepareCursorControllers(); } private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, @@ -6638,11 +6300,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener r.bottom += verticalOffset; } - private int viewportToContentHorizontalOffset() { + int viewportToContentHorizontalOffset() { return getCompoundPaddingLeft() - mScrollX; } - private int viewportToContentVerticalOffset() { + int viewportToContentVerticalOffset() { int offset = getExtendedPaddingTop() - mScrollY; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { offset += getVerticalOffset(false); @@ -6855,18 +6517,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener getEditor().mCursorVisible = visible; invalidate(); - makeBlink(); + getEditor().makeBlink(); // InsertionPointCursorController depends on mCursorVisible - prepareCursorControllers(); + getEditor().prepareCursorControllers(); } } - private boolean isCursorVisible() { - // The default value is true, even when there is no associated Editor - return mEditor == null ? true : (getEditor().mCursorVisible && isTextEditable()); - } - private boolean canMarquee() { int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()); return width > 0 && (mLayout.getLineWidth(0) > width || @@ -7049,12 +6706,29 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + void updateAfterEdit() { + invalidate(); + int curs = getSelectionStart(); + + if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { + registerForPreDraw(); + } + + if (curs >= 0) { + mHighlightPathBogus = true; + if (mEditor != null) getEditor().makeBlink(); + bringPointIntoView(curs); + } + + checkForResize(); + } + /** * Not private so it can be called from an inner class without going * through a thunk. */ void handleTextChanged(CharSequence buffer, int start, int before, int after) { - final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; + final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; if (ims == null || ims.mBatchEditNesting == 0) { updateAfterEdit(); } @@ -7085,7 +6759,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean selChanged = false; int newSelStart=-1, newSelEnd=-1; - final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; + final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState; if (what == Selection.SELECTION_END) { selChanged = true; @@ -7094,7 +6768,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (oldStart >= 0 || newStart >= 0) { invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart); registerForPreDraw(); - makeBlink(); + if (mEditor != null) getEditor().makeBlink(); } } @@ -7186,20 +6860,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Create new SpellCheckSpans on the modified region. - */ - private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) { - if (isTextEditable() && isSuggestionsEnabled() && !(this instanceof ExtractEditText)) { - if (getEditor().mSpellChecker == null && createSpellChecker) { - getEditor().mSpellChecker = new SpellChecker(this); - } - if (getEditor().mSpellChecker != null) { - getEditor().mSpellChecker.spellCheck(start, end); - } - } - } - - /** * @hide */ @Override @@ -7219,7 +6879,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Because of View recycling in ListView, there is no easy way to know when a TextView with // selection becomes visible again. Until a better solution is found, stop text selection // mode (if any) as soon as this TextView is recycled. - if (mEditor != null) hideControllers(); + if (mEditor != null) getEditor().hideControllers(); } @Override @@ -7269,7 +6929,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (mEditor != null && visibility != VISIBLE) { - hideControllers(); + getEditor().hideControllers(); } } @@ -7350,31 +7010,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener handled |= imm != null && imm.showSoftInput(this, 0); } - boolean selectAllGotFocus = getEditor().mSelectAllOnFocus && didTouchFocusSelect(); - hideControllers(); - if (!selectAllGotFocus && mText.length() > 0) { - // Move cursor - final int offset = getOffsetForPosition(event.getX(), event.getY()); - Selection.setSelection((Spannable) mText, offset); - if (getEditor().mSpellChecker != null) { - // When the cursor moves, the word that was typed may need spell check - getEditor().mSpellChecker.onSelectionChanged(); - } - if (!extractedTextModeWillBeStarted()) { - if (isCursorInsideEasyCorrectionSpan()) { - getEditor().mShowSuggestionRunnable = new Runnable() { - public void run() { - showSuggestions(); - } - }; - // removeCallbacks is performed on every touch - postDelayed(getEditor().mShowSuggestionRunnable, - ViewConfiguration.getDoubleTapTimeout()); - } else if (hasInsertionController()) { - getInsertionController().show(); - } - } - } + // The above condition ensures that the mEditor is not null + getEditor().onTouchUpEvent(event); handled = true; } @@ -7387,53 +7024,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } - /** - * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}. - */ - private boolean isCursorInsideSuggestionSpan() { - if (!(mText instanceof Spannable)) return false; - - SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(), - getSelectionEnd(), SuggestionSpan.class); - return (suggestionSpans.length > 0); - } - - /** - * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with - * {@link SuggestionSpan#FLAG_EASY_CORRECT} set. - */ - private boolean isCursorInsideEasyCorrectionSpan() { - Spannable spannable = (Spannable) mText; - SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(), - getSelectionEnd(), SuggestionSpan.class); - for (int i = 0; i < suggestionSpans.length; i++) { - if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) { - return true; - } - } - return false; - } - - /** - * Downgrades to simple suggestions all the easy correction spans that are not a spell check - * span. - */ - private void downgradeEasyCorrectionSpans() { - if (mText instanceof Spannable) { - Spannable spannable = (Spannable) mText; - SuggestionSpan[] suggestionSpans = spannable.getSpans(0, - spannable.length(), SuggestionSpan.class); - for (int i = 0; i < suggestionSpans.length; i++) { - int flags = suggestionSpans[i].getFlags(); - if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0 - && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) { - flags &= ~SuggestionSpan.FLAG_EASY_CORRECT; - suggestionSpans[i].setFlags(flags); - } - } - } - } - @Override public boolean onGenericMotionEvent(MotionEvent event) { if (mMovement != null && mText instanceof Spannable && mLayout != null) { @@ -7450,44 +7040,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onGenericMotionEvent(event); } - private void prepareCursorControllers() { - if (mEditor == null) return; - - boolean windowSupportsHandles = false; - - ViewGroup.LayoutParams params = getRootView().getLayoutParams(); - if (params instanceof WindowManager.LayoutParams) { - WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params; - windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW - || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW; - } - - getEditor().mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null; - getEditor().mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() && - mLayout != null; - - if (!getEditor().mInsertionControllerEnabled) { - hideInsertionPointCursorController(); - if (getEditor().mInsertionPointCursorController != null) { - getEditor().mInsertionPointCursorController.onDetached(); - getEditor().mInsertionPointCursorController = null; - } - } - - if (!getEditor().mSelectionControllerEnabled) { - stopSelectionActionMode(); - if (getEditor().mSelectionModifierCursorController != null) { - getEditor().mSelectionModifierCursorController.onDetached(); - getEditor().mSelectionModifierCursorController = null; - } - } - } - /** * @return True iff this TextView contains a text that can be edited, or if this is * a selectable TextView. */ - private boolean isTextEditable() { + boolean isTextEditable() { return mText instanceof Editable && onCheckIsTextEditor() && isEnabled(); } @@ -7522,32 +7079,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mScroller = s; } - /** - * @return True when the TextView isFocused and has a valid zero-length selection (cursor). - */ - private boolean shouldBlink() { - if (mEditor == null || !isCursorVisible() || !isFocused()) return false; - - final int start = getSelectionStart(); - if (start < 0) return false; - - final int end = getSelectionEnd(); - if (end < 0) return false; - - return start == end; - } - - private void makeBlink() { - if (shouldBlink()) { - getEditor().mShowCursor = SystemClock.uptimeMillis(); - if (getEditor().mBlink == null) getEditor().mBlink = new Blink(this); - getEditor().mBlink.removeCallbacks(getEditor().mBlink); - getEditor().mBlink.postAtTime(getEditor().mBlink, getEditor().mShowCursor + BLINK); - } else { - if (mEditor != null && getEditor().mBlink != null) getEditor().mBlink.removeCallbacks(getEditor().mBlink); - } - } - @Override protected float getLeftFadingEdgeStrength() { if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f; @@ -7726,10 +7257,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have - * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient. + * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not sufficient. */ private boolean canSelectText() { - return hasSelectionController() && mText.length() != 0; + return mText.length() != 0 && mEditor != null && getEditor().hasSelectionController(); } /** @@ -7738,7 +7269,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * See also {@link #canSelectText()}. */ - private boolean textCanBeSelected() { + boolean textCanBeSelected() { // prepareCursorController() relies on this method. // If you change this condition, make sure prepareCursorController is called anywhere // the value of this condition might be changed. @@ -7746,112 +7277,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return isTextEditable() || (isTextSelectable() && mText instanceof Spannable && isEnabled()); } - private boolean canCut() { - if (hasPasswordTransformationMethod()) { - return false; - } - - if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) { - return true; - } - - return false; - } - - private boolean canCopy() { - if (hasPasswordTransformationMethod()) { - return false; - } - - if (mText.length() > 0 && hasSelection()) { - return true; - } - - return false; - } - - private boolean canPaste() { - return (mText instanceof Editable && - mEditor != null && getEditor().mKeyListener != null && - getSelectionStart() >= 0 && - getSelectionEnd() >= 0 && - ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)). - hasPrimaryClip()); - } - - private boolean selectAll() { - final int length = mText.length(); - Selection.setSelection((Spannable) mText, 0, length); - return length > 0; - } - - /** - * Adjusts selection to the word under last touch offset. - * Return true if the operation was successfully performed. - */ - private boolean selectCurrentWord() { - if (!canSelectText()) { - return false; - } - - if (hasPasswordTransformationMethod()) { - // Always select all on a password field. - // Cut/copy menu entries are not available for passwords, but being able to select all - // is however useful to delete or paste to replace the entire content. - return selectAll(); - } - - int inputType = getInputType(); - int klass = inputType & InputType.TYPE_MASK_CLASS; - int variation = inputType & InputType.TYPE_MASK_VARIATION; - - // Specific text field types: select the entire text for these - if (klass == InputType.TYPE_CLASS_NUMBER || - klass == InputType.TYPE_CLASS_PHONE || - klass == InputType.TYPE_CLASS_DATETIME || - variation == InputType.TYPE_TEXT_VARIATION_URI || - variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - return selectAll(); - } - - long lastTouchOffsets = getLastTouchOffsets(); - final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets); - final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets); - - // Safety check in case standard touch event handling has been bypassed - if (minOffset < 0 || minOffset >= mText.length()) return false; - if (maxOffset < 0 || maxOffset >= mText.length()) return false; - - int selectionStart, selectionEnd; - - // If a URLSpan (web address, email, phone...) is found at that position, select it. - URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class); - if (urlSpans.length >= 1) { - URLSpan urlSpan = urlSpans[0]; - selectionStart = ((Spanned) mText).getSpanStart(urlSpan); - selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan); - } else { - final WordIterator wordIterator = getWordIterator(); - wordIterator.setCharSequence(mText, minOffset, maxOffset); - - selectionStart = wordIterator.getBeginning(minOffset); - selectionEnd = wordIterator.getEnd(maxOffset); - - if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE || - selectionStart == selectionEnd) { - // Possible when the word iterator does not properly handle the text's language - long range = getCharRange(minOffset); - selectionStart = TextUtils.unpackRangeStartFromLong(range); - selectionEnd = TextUtils.unpackRangeEndFromLong(range); - } - } - - Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); - return selectionEnd > selectionStart; - } - /** * This is a temporary method. Future versions may support multi-locale text. * @@ -7877,45 +7302,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * This method is used by the ArrowKeyMovementMethod to jump from one word to the other. + * Made available to achieve a consistent behavior. * @hide */ public WordIterator getWordIterator() { - if (getEditor().mWordIterator == null) { - getEditor().mWordIterator = new WordIterator(getTextServicesLocale()); - } - return getEditor().mWordIterator; - } - - private long getCharRange(int offset) { - final int textLength = mText.length(); - if (offset + 1 < textLength) { - final char currentChar = mText.charAt(offset); - final char nextChar = mText.charAt(offset + 1); - if (Character.isSurrogatePair(currentChar, nextChar)) { - return TextUtils.packRangeInLong(offset, offset + 2); - } - } - if (offset < textLength) { - return TextUtils.packRangeInLong(offset, offset + 1); - } - if (offset - 2 >= 0) { - final char previousChar = mText.charAt(offset - 1); - final char previousPreviousChar = mText.charAt(offset - 2); - if (Character.isSurrogatePair(previousPreviousChar, previousChar)) { - return TextUtils.packRangeInLong(offset - 2, offset); - } - } - if (offset - 1 >= 0) { - return TextUtils.packRangeInLong(offset - 1, offset); + if (getEditor() != null) { + return mEditor.getWordIterator(); + } else { + return null; } - return TextUtils.packRangeInLong(offset, offset); - } - - private long getLastTouchOffsets() { - SelectionModifierCursorController selectionController = getSelectionController(); - final int minOffset = selectionController.getMinTouchOffset(); - final int maxOffset = selectionController.getMaxTouchOffset(); - return TextUtils.packRangeInLong(minOffset, maxOffset); } @Override @@ -8004,11 +7400,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return imm != null && imm.isActive(this); } - // Selection context mode - private static final int ID_SELECT_ALL = android.R.id.selectAll; - private static final int ID_CUT = android.R.id.cut; - private static final int ID_COPY = android.R.id.copy; - private static final int ID_PASTE = android.R.id.paste; + static final int ID_SELECT_ALL = android.R.id.selectAll; + static final int ID_CUT = android.R.id.cut; + static final int ID_COPY = android.R.id.copy; + static final int ID_PASTE = android.R.id.paste; /** * Called when a context menu option for the text view is selected. Currently @@ -8033,7 +7428,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case ID_SELECT_ALL: // This does not enter text selection mode. Text is highlighted, so that it can be // bulk edited, like selectAllOnFocus does. Returns true even if text is empty. - selectAll(); + selectAllText(); return true; case ID_PASTE: @@ -8054,89 +7449,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - private CharSequence getTransformedText(int start, int end) { + CharSequence getTransformedText(int start, int end) { return removeSuggestionSpans(mTransformed.subSequence(start, end)); } - /** - * Prepare text so that there are not zero or two spaces at beginning and end of region defined - * by [min, max] when replacing this region by paste. - * Note that if there were two spaces (or more) at that position before, they are kept. We just - * make sure we do not add an extra one from the paste content. - */ - private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) { - if (paste.length() > 0) { - if (min > 0) { - final char charBefore = mTransformed.charAt(min - 1); - final char charAfter = paste.charAt(0); - - if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { - // Two spaces at beginning of paste: remove one - final int originalLength = mText.length(); - deleteText_internal(min - 1, min); - // Due to filters, there is no guarantee that exactly one character was - // removed: count instead. - final int delta = mText.length() - originalLength; - min += delta; - max += delta; - } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && - !Character.isSpaceChar(charAfter) && charAfter != '\n') { - // No space at beginning of paste: add one - final int originalLength = mText.length(); - replaceText_internal(min, min, " "); - // Taking possible filters into account as above. - final int delta = mText.length() - originalLength; - min += delta; - max += delta; - } - } - - if (max < mText.length()) { - final char charBefore = paste.charAt(paste.length() - 1); - final char charAfter = mTransformed.charAt(max); - - if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { - // Two spaces at end of paste: remove one - deleteText_internal(max, max + 1); - } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && - !Character.isSpaceChar(charAfter) && charAfter != '\n') { - // No space at end of paste: add one - replaceText_internal(max, max, " "); - } - } - } - - return TextUtils.packRangeInLong(min, max); - } - - private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) { - TextView shadowView = (TextView) inflate(mContext, - com.android.internal.R.layout.text_drag_thumbnail, null); - - if (shadowView == null) { - throw new IllegalArgumentException("Unable to inflate text drag thumbnail"); - } - - if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) { - text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH); - } - shadowView.setText(text); - shadowView.setTextColor(getTextColors()); - - shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge); - shadowView.setGravity(Gravity.CENTER); - - shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - - final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - shadowView.measure(size, size); - - shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight()); - shadowView.invalidate(); - return new DragShadowBuilder(shadowView); - } - @Override public boolean performLongClick() { boolean handled = false; @@ -8145,179 +7461,27 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener handled = true; } - if (mEditor == null) { - return handled; - } - - // Long press in empty space moves cursor and shows the Paste affordance if available. - if (!handled && !isPositionOnText(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY) && - getEditor().mInsertionControllerEnabled) { - final int offset = getOffsetForPosition(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY); - stopSelectionActionMode(); - Selection.setSelection((Spannable) mText, offset); - getInsertionController().showWithActionPopup(); - handled = true; - } - - if (!handled && getEditor().mSelectionActionMode != null) { - if (touchPositionIsInSelection()) { - // Start a drag - final int start = getSelectionStart(); - final int end = getSelectionEnd(); - CharSequence selectedText = getTransformedText(start, end); - ClipData data = ClipData.newPlainText(null, selectedText); - DragLocalState localState = new DragLocalState(this, start, end); - startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); - stopSelectionActionMode(); - } else { - getSelectionController().hide(); - selectCurrentWord(); - getSelectionController().show(); - } - handled = true; - } - - // Start a new selection - if (!handled) { - handled = startSelectionActionMode(); + if (mEditor != null) { + handled |= getEditor().performLongClick(handled); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - getEditor().mDiscardNextActionUp = true; + if (mEditor != null) getEditor().mDiscardNextActionUp = true; } return handled; } - private boolean touchPositionIsInSelection() { - int selectionStart = getSelectionStart(); - int selectionEnd = getSelectionEnd(); - - if (selectionStart == selectionEnd) { - return false; - } - - if (selectionStart > selectionEnd) { - int tmp = selectionStart; - selectionStart = selectionEnd; - selectionEnd = tmp; - Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); - } - - SelectionModifierCursorController selectionController = getSelectionController(); - int minOffset = selectionController.getMinTouchOffset(); - int maxOffset = selectionController.getMaxTouchOffset(); - - return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); - } - - private PositionListener getPositionListener() { - if (getEditor().mPositionListener == null) { - getEditor().mPositionListener = new PositionListener(); - } - return getEditor().mPositionListener; - } - - private interface TextViewPositionListener { - public void updatePosition(int parentPositionX, int parentPositionY, - boolean parentPositionChanged, boolean parentScrolled); - } - - private boolean isPositionVisible(int positionX, int positionY) { - synchronized (TEMP_POSITION) { - final float[] position = TEMP_POSITION; - position[0] = positionX; - position[1] = positionY; - View view = this; - - while (view != null) { - if (view != this) { - // Local scroll is already taken into account in positionX/Y - position[0] -= view.getScrollX(); - position[1] -= view.getScrollY(); - } - - if (position[0] < 0 || position[1] < 0 || - position[0] > view.getWidth() || position[1] > view.getHeight()) { - return false; - } - - if (!view.getMatrix().isIdentity()) { - view.getMatrix().mapPoints(position); - } - - position[0] += view.getLeft(); - position[1] += view.getTop(); - - final ViewParent parent = view.getParent(); - if (parent instanceof View) { - view = (View) parent; - } else { - // We've reached the ViewRoot, stop iterating - view = null; - } - } - } - - // We've been able to walk up the view hierarchy and the position was never clipped - return true; - } - - private boolean isOffsetVisible(int offset) { - final int line = mLayout.getLineForOffset(offset); - final int lineBottom = mLayout.getLineBottom(line); - final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset); - return isPositionVisible(primaryHorizontal + viewportToContentHorizontalOffset(), - lineBottom + viewportToContentVerticalOffset()); - } - @Override protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) { super.onScrollChanged(horiz, vert, oldHoriz, oldVert); if (mEditor != null) { - if (getEditor().mPositionListener != null) { - getEditor().mPositionListener.onScrollChanged(); - } - // Internal scroll affects the clip boundaries - getEditor().invalidateTextDisplayList(); + getEditor().onScrollChanged(); } } /** - * Removes the suggestion spans. - */ - CharSequence removeSuggestionSpans(CharSequence text) { - if (text instanceof Spanned) { - Spannable spannable; - if (text instanceof Spannable) { - spannable = (Spannable) text; - } else { - spannable = new SpannableString(text); - text = spannable; - } - - SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class); - for (int i = 0; i < spans.length; i++) { - spannable.removeSpan(spans[i]); - } - } - return text; - } - - void showSuggestions() { - if (getEditor().mSuggestionsPopupWindow == null) { - getEditor().mSuggestionsPopupWindow = new SuggestionsPopupWindow(); - } - hideControllers(); - getEditor().mSuggestionsPopupWindow.show(); - } - - boolean areSuggestionsShown() { - return getEditor().mSuggestionsPopupWindow != null && getEditor().mSuggestionsPopupWindow.isShowing(); - } - - /** * Return whether or not suggestions are enabled on this TextView. The suggestions are generated * by the IME or by the spell checker as the user types. This is done by adding * {@link SuggestionSpan}s to the text. @@ -8391,65 +7555,100 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * - * @return true if the selection mode was actually started. + * @hide */ - private boolean startSelectionActionMode() { - if (getEditor().mSelectionActionMode != null) { - // Selection action mode is already started - return false; - } + protected void stopSelectionActionMode() { + getEditor().stopSelectionActionMode(); + } - if (!canSelectText() || !requestFocus()) { - Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled."); + boolean canCut() { + if (hasPasswordTransformationMethod()) { return false; } - if (!hasSelection()) { - // There may already be a selection on device rotation - if (!selectCurrentWord()) { - // No word found under cursor or text selection not permitted. - return false; - } + if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) { + return true; } - boolean willExtract = extractedTextModeWillBeStarted(); + return false; + } - // Do not start the action mode when extracted text will show up full screen, which would - // immediately hide the newly created action bar and would be visually distracting. - if (!willExtract) { - ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); - getEditor().mSelectionActionMode = startActionMode(actionModeCallback); + boolean canCopy() { + if (hasPasswordTransformationMethod()) { + return false; } - final boolean selectionStarted = getEditor().mSelectionActionMode != null || willExtract; - if (selectionStarted && !isTextSelectable()) { - // Show the IME to be able to replace text, except when selecting non editable text. - final InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - imm.showSoftInput(this, 0, null); - } + if (mText.length() > 0 && hasSelection()) { + return true; } - return selectionStarted; + return false; } - private boolean extractedTextModeWillBeStarted() { - if (!(this instanceof ExtractEditText)) { - final InputMethodManager imm = InputMethodManager.peekInstance(); - return imm != null && imm.isFullscreenMode(); - } - return false; + boolean canPaste() { + return (mText instanceof Editable && + mEditor != null && getEditor().mKeyListener != null && + getSelectionStart() >= 0 && + getSelectionEnd() >= 0 && + ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)). + hasPrimaryClip()); + } + + boolean selectAllText() { + final int length = mText.length(); + Selection.setSelection((Spannable) mText, 0, length); + return length > 0; } /** - * @hide + * Prepare text so that there are not zero or two spaces at beginning and end of region defined + * by [min, max] when replacing this region by paste. + * Note that if there were two spaces (or more) at that position before, they are kept. We just + * make sure we do not add an extra one from the paste content. */ - protected void stopSelectionActionMode() { - if (getEditor().mSelectionActionMode != null) { - // This will hide the mSelectionModifierCursorController - getEditor().mSelectionActionMode.finish(); + long prepareSpacesAroundPaste(int min, int max, CharSequence paste) { + if (paste.length() > 0) { + if (min > 0) { + final char charBefore = mTransformed.charAt(min - 1); + final char charAfter = paste.charAt(0); + + if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { + // Two spaces at beginning of paste: remove one + final int originalLength = mText.length(); + deleteText_internal(min - 1, min); + // Due to filters, there is no guarantee that exactly one character was + // removed: count instead. + final int delta = mText.length() - originalLength; + min += delta; + max += delta; + } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && + !Character.isSpaceChar(charAfter) && charAfter != '\n') { + // No space at beginning of paste: add one + final int originalLength = mText.length(); + replaceText_internal(min, min, " "); + // Taking possible filters into account as above. + final int delta = mText.length() - originalLength; + min += delta; + max += delta; + } + } + + if (max < mText.length()) { + final char charBefore = paste.charAt(paste.length() - 1); + final char charAfter = mTransformed.charAt(max); + + if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) { + // Two spaces at end of paste: remove one + deleteText_internal(max, max + 1); + } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' && + !Character.isSpaceChar(charAfter) && charAfter != '\n') { + // No space at end of paste: add one + replaceText_internal(max, max, " "); + } + } } + + return TextUtils.packRangeInLong(min, max); } /** @@ -8489,36 +7688,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener LAST_CUT_OR_COPY_TIME = SystemClock.uptimeMillis(); } - private void hideInsertionPointCursorController() { - // No need to create the controller to hide it. - if (getEditor().mInsertionPointCursorController != null) { - getEditor().mInsertionPointCursorController.hide(); - } - } - - /** - * Hides the insertion controller and stops text selection mode, hiding the selection controller - */ - private void hideControllers() { - hideCursorControllers(); - hideSpanControllers(); - } - - private void hideSpanControllers() { - if (mChangeWatcher != null) { - mChangeWatcher.hideControllers(); - } - } - - private void hideCursorControllers() { - if (getEditor().mSuggestionsPopupWindow != null && !getEditor().mSuggestionsPopupWindow.isShowingUp()) { - // Should be done before hide insertion point controller since it triggers a show of it - getEditor().mSuggestionsPopupWindow.hide(); - } - hideInsertionPointCursorController(); - stopSelectionActionMode(); - } - /** * Get the character offset closest to the specified absolute position. A typical use case is to * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method. @@ -8535,7 +7704,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return offset; } - private float convertToLocalHorizontalCoordinate(float x) { + float convertToLocalHorizontalCoordinate(float x) { x -= getTotalPaddingLeft(); // Clamp the position to inside of the view. x = Math.max(0.0f, x); @@ -8544,7 +7713,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return x; } - private int getLineAtCoordinate(float y) { + int getLineAtCoordinate(float y) { y -= getTotalPaddingTop(); // Clamp the position to inside of the view. y = Math.max(0.0f, y); @@ -8558,25 +7727,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return getLayout().getOffsetForHorizontal(line, x); } - /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed - * in the view. Returns false when the position is in the empty space of left/right of text. - */ - private boolean isPositionOnText(float x, float y) { - if (getLayout() == null) return false; - - final int line = getLineAtCoordinate(y); - x = convertToLocalHorizontalCoordinate(x); - - if (x < getLayout().getLineLeft(line)) return false; - if (x > getLayout().getLineRight(line)) return false; - return true; - } - @Override public boolean onDragEvent(DragEvent event) { switch (event.getAction()) { case DragEvent.ACTION_DRAG_STARTED: - return mEditor != null && hasInsertionController(); + return mEditor != null && getEditor().hasInsertionController(); case DragEvent.ACTION_DRAG_ENTERED: TextView.this.requestFocus(); @@ -8588,7 +7743,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; case DragEvent.ACTION_DROP: - onDrop(event); + if (mEditor != null) getEditor().onDrop(event); return true; case DragEvent.ACTION_DRAG_ENDED: @@ -8598,112 +7753,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void onDrop(DragEvent event) { - StringBuilder content = new StringBuilder(""); - ClipData clipData = event.getClipData(); - final int itemCount = clipData.getItemCount(); - for (int i=0; i < itemCount; i++) { - Item item = clipData.getItemAt(i); - content.append(item.coerceToText(TextView.this.mContext)); - } - - final int offset = getOffsetForPosition(event.getX(), event.getY()); - - Object localState = event.getLocalState(); - DragLocalState dragLocalState = null; - if (localState instanceof DragLocalState) { - dragLocalState = (DragLocalState) localState; - } - boolean dragDropIntoItself = dragLocalState != null && - dragLocalState.sourceTextView == this; - - if (dragDropIntoItself) { - if (offset >= dragLocalState.start && offset < dragLocalState.end) { - // A drop inside the original selection discards the drop. - return; - } - } - - final int originalLength = mText.length(); - long minMax = prepareSpacesAroundPaste(offset, offset, content); - int min = TextUtils.unpackRangeStartFromLong(minMax); - int max = TextUtils.unpackRangeEndFromLong(minMax); - - Selection.setSelection((Spannable) mText, max); - replaceText_internal(min, max, content); - - if (dragDropIntoItself) { - int dragSourceStart = dragLocalState.start; - int dragSourceEnd = dragLocalState.end; - if (max <= dragSourceStart) { - // Inserting text before selection has shifted positions - final int shift = mText.length() - originalLength; - dragSourceStart += shift; - dragSourceEnd += shift; - } - - // Delete original selection - deleteText_internal(dragSourceStart, dragSourceEnd); - - // Make sure we do not leave two adjacent spaces. - if ((dragSourceStart == 0 || - Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) && - (dragSourceStart == mText.length() || - Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) { - final int pos = dragSourceStart == mText.length() ? - dragSourceStart - 1 : dragSourceStart; - deleteText_internal(pos, pos + 1); - } - } - } - - /** - * @return True if this view supports insertion handles. - */ - boolean hasInsertionController() { - return getEditor().mInsertionControllerEnabled; - } - - /** - * @return True if this view supports selection handles. - */ - boolean hasSelectionController() { - return getEditor().mSelectionControllerEnabled; - } - - InsertionPointCursorController getInsertionController() { - if (!getEditor().mInsertionControllerEnabled) { - return null; - } - - if (getEditor().mInsertionPointCursorController == null) { - getEditor().mInsertionPointCursorController = new InsertionPointCursorController(); - - final ViewTreeObserver observer = getViewTreeObserver(); - observer.addOnTouchModeChangeListener(getEditor().mInsertionPointCursorController); - } - - return getEditor().mInsertionPointCursorController; - } - - SelectionModifierCursorController getSelectionController() { - if (!getEditor().mSelectionControllerEnabled) { - return null; - } - - if (getEditor().mSelectionModifierCursorController == null) { - getEditor().mSelectionModifierCursorController = new SelectionModifierCursorController(); - - final ViewTreeObserver observer = getViewTreeObserver(); - observer.addOnTouchModeChangeListener(getEditor().mSelectionModifierCursorController); - } - - return getEditor().mSelectionModifierCursorController; - } - boolean isInBatchEditMode() { if (mEditor == null) return false; - final InputMethodState ims = getEditor().mInputMethodState; + final Editor.InputMethodState ims = getEditor().mInputMethodState; if (ims != null) { return ims.mBatchEditNesting > 0; } @@ -8864,7 +7916,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (!(this instanceof EditText)) { Log.e(LOG_TAG + " EDITOR", "Creating Editor on TextView. " + reason); } - mEditor = new Editor(); + mEditor = new Editor(this); } else { if (!(this instanceof EditText)) { Log.d(LOG_TAG + " EDITOR", "Redundant Editor creation. " + reason); @@ -9041,151 +8093,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private static class ErrorPopup extends PopupWindow { - private boolean mAbove = false; - private final TextView mView; - private int mPopupInlineErrorBackgroundId = 0; - private int mPopupInlineErrorAboveBackgroundId = 0; - - ErrorPopup(TextView v, int width, int height) { - super(v, width, height); - mView = v; - // Make sure the TextView has a background set as it will be used the first time it is - // shown and positionned. Initialized with below background, which should have - // dimensions identical to the above version for this to work (and is more likely). - mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, - com.android.internal.R.styleable.Theme_errorMessageBackground); - mView.setBackgroundResource(mPopupInlineErrorBackgroundId); - } - - void fixDirection(boolean above) { - mAbove = above; - - if (above) { - mPopupInlineErrorAboveBackgroundId = - getResourceId(mPopupInlineErrorAboveBackgroundId, - com.android.internal.R.styleable.Theme_errorMessageAboveBackground); - } else { - mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, - com.android.internal.R.styleable.Theme_errorMessageBackground); - } - - mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId : - mPopupInlineErrorBackgroundId); - } - - private int getResourceId(int currentId, int index) { - if (currentId == 0) { - TypedArray styledAttributes = mView.getContext().obtainStyledAttributes( - R.styleable.Theme); - currentId = styledAttributes.getResourceId(index, 0); - styledAttributes.recycle(); - } - return currentId; - } - - @Override - public void update(int x, int y, int w, int h, boolean force) { - super.update(x, y, w, h, force); - - boolean above = isAboveAnchor(); - if (above != mAbove) { - fixDirection(above); - } - } - } - - private class CorrectionHighlighter { - private final Path mPath = new Path(); - private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private int mStart, mEnd; - private long mFadingStartTime; - private final static int FADE_OUT_DURATION = 400; - - public CorrectionHighlighter() { - mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale); - mPaint.setStyle(Paint.Style.FILL); - } - - public void highlight(CorrectionInfo info) { - mStart = info.getOffset(); - mEnd = mStart + info.getNewText().length(); - mFadingStartTime = SystemClock.uptimeMillis(); - - if (mStart < 0 || mEnd < 0) { - stopAnimation(); - } - } - - public void draw(Canvas canvas, int cursorOffsetVertical) { - if (updatePath() && updatePaint()) { - if (cursorOffsetVertical != 0) { - canvas.translate(0, cursorOffsetVertical); - } - - canvas.drawPath(mPath, mPaint); - - if (cursorOffsetVertical != 0) { - canvas.translate(0, -cursorOffsetVertical); - } - invalidate(true); // TODO invalidate cursor region only - } else { - stopAnimation(); - invalidate(false); // TODO invalidate cursor region only - } - } - - private boolean updatePaint() { - final long duration = SystemClock.uptimeMillis() - mFadingStartTime; - if (duration > FADE_OUT_DURATION) return false; - - final float coef = 1.0f - (float) duration / FADE_OUT_DURATION; - final int highlightColorAlpha = Color.alpha(mHighlightColor); - final int color = (mHighlightColor & 0x00FFFFFF) + - ((int) (highlightColorAlpha * coef) << 24); - mPaint.setColor(color); - return true; - } - - private boolean updatePath() { - final Layout layout = TextView.this.mLayout; - if (layout == null) return false; - - // Update in case text is edited while the animation is run - final int length = mText.length(); - int start = Math.min(length, mStart); - int end = Math.min(length, mEnd); - - mPath.reset(); - TextView.this.mLayout.getSelectionPath(start, end, mPath); - return true; - } - - private void invalidate(boolean delayed) { - if (TextView.this.mLayout == null) return; - - synchronized (TEMP_RECTF) { - mPath.computeBounds(TEMP_RECTF, false); - - int left = getCompoundPaddingLeft(); - int top = getExtendedPaddingTop() + getVerticalOffset(true); - - if (delayed) { - TextView.this.postInvalidateOnAnimation( - left + (int) TEMP_RECTF.left, top + (int) TEMP_RECTF.top, - left + (int) TEMP_RECTF.right, top + (int) TEMP_RECTF.bottom); - } else { - TextView.this.postInvalidate((int) TEMP_RECTF.left, (int) TEMP_RECTF.top, - (int) TEMP_RECTF.right, (int) TEMP_RECTF.bottom); - } - } - } - - private void stopAnimation() { - TextView.this.getEditor().mCorrectionHighlighter = null; - } - } - private static final class Marquee extends Handler { // TODO: Add an option to configure this private static final float MARQUEE_DELTA_MAX = 0.07f; @@ -9322,217 +8229,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - /** - * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related - * pop-up should be displayed. - */ - private class EasyEditSpanController { - - private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs - - private EasyEditPopupWindow mPopupWindow; - - private EasyEditSpan mEasyEditSpan; - - private Runnable mHidePopup; - - private void hide() { - if (mPopupWindow != null) { - mPopupWindow.hide(); - TextView.this.removeCallbacks(mHidePopup); - } - removeSpans(mText); - mEasyEditSpan = null; - } - - /** - * Monitors the changes in the text. - * - * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used, - * as the notifications are not sent when a spannable (with spans) is inserted. - */ - public void onTextChange(CharSequence buffer) { - adjustSpans(mText); - - if (getWindowVisibility() != View.VISIBLE) { - // The window is not visible yet, ignore the text change. - return; - } - - if (mLayout == null) { - // The view has not been layout yet, ignore the text change - return; - } - - InputMethodManager imm = InputMethodManager.peekInstance(); - if (!(TextView.this instanceof ExtractEditText) - && imm != null && imm.isFullscreenMode()) { - // The input is in extract mode. We do not have to handle the easy edit in the - // original TextView, as the ExtractEditText will do - return; - } - - // Remove the current easy edit span, as the text changed, and remove the pop-up - // (if any) - if (mEasyEditSpan != null) { - if (mText instanceof Spannable) { - ((Spannable) mText).removeSpan(mEasyEditSpan); - } - mEasyEditSpan = null; - } - if (mPopupWindow != null && mPopupWindow.isShowing()) { - mPopupWindow.hide(); - } - - // Display the new easy edit span (if any). - if (buffer instanceof Spanned) { - mEasyEditSpan = getSpan((Spanned) buffer); - if (mEasyEditSpan != null) { - if (mPopupWindow == null) { - mPopupWindow = new EasyEditPopupWindow(); - mHidePopup = new Runnable() { - @Override - public void run() { - hide(); - } - }; - } - mPopupWindow.show(mEasyEditSpan); - TextView.this.removeCallbacks(mHidePopup); - TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); - } - } - } - - /** - * Adjusts the spans by removing all of them except the last one. - */ - private void adjustSpans(CharSequence buffer) { - // This method enforces that only one easy edit span is attached to the text. - // A better way to enforce this would be to listen for onSpanAdded, but this method - // cannot be used in this scenario as no notification is triggered when a text with - // spans is inserted into a text. - if (buffer instanceof Spannable) { - Spannable spannable = (Spannable) buffer; - EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(), - EasyEditSpan.class); - for (int i = 0; i < spans.length - 1; i++) { - spannable.removeSpan(spans[i]); - } - } - } - - /** - * Removes all the {@link EasyEditSpan} currently attached. - */ - private void removeSpans(CharSequence buffer) { - if (buffer instanceof Spannable) { - Spannable spannable = (Spannable) buffer; - EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(), - EasyEditSpan.class); - for (int i = 0; i < spans.length; i++) { - spannable.removeSpan(spans[i]); - } - } - } - - private EasyEditSpan getSpan(Spanned spanned) { - EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(), - EasyEditSpan.class); - if (easyEditSpans.length == 0) { - return null; - } else { - return easyEditSpans[0]; - } - } - } - - /** - * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled - * by {@link EasyEditSpanController}. - */ - private class EasyEditPopupWindow extends PinnedPopupWindow - implements OnClickListener { - private static final int POPUP_TEXT_LAYOUT = - com.android.internal.R.layout.text_edit_action_popup_text; - private TextView mDeleteTextView; - private EasyEditSpan mEasyEditSpan; - - @Override - protected void createPopupWindow() { - mPopupWindow = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - mPopupWindow.setClippingEnabled(true); - } - - @Override - protected void initContentView() { - LinearLayout linearLayout = new LinearLayout(TextView.this.getContext()); - linearLayout.setOrientation(LinearLayout.HORIZONTAL); - mContentView = linearLayout; - mContentView.setBackgroundResource( - com.android.internal.R.drawable.text_edit_side_paste_window); - - LayoutInflater inflater = (LayoutInflater)TextView.this.mContext. - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - LayoutParams wrapContent = new LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mDeleteTextView.setLayoutParams(wrapContent); - mDeleteTextView.setText(com.android.internal.R.string.delete); - mDeleteTextView.setOnClickListener(this); - mContentView.addView(mDeleteTextView); - } - - public void show(EasyEditSpan easyEditSpan) { - mEasyEditSpan = easyEditSpan; - super.show(); - } - - @Override - public void onClick(View view) { - if (view == mDeleteTextView) { - Editable editable = (Editable) mText; - int start = editable.getSpanStart(mEasyEditSpan); - int end = editable.getSpanEnd(mEasyEditSpan); - if (start >= 0 && end >= 0) { - deleteText_internal(start, end); - } - } - } - - @Override - protected int getTextOffset() { - // Place the pop-up at the end of the span - Editable editable = (Editable) mText; - return editable.getSpanEnd(mEasyEditSpan); - } - - @Override - protected int getVerticalLocalPosition(int line) { - return mLayout.getLineBottom(line); - } - - @Override - protected int clipVertically(int positionY) { - // As we display the pop-up below the span, no vertical clipping is required. - return positionY; - } - } - private class ChangeWatcher implements TextWatcher, SpanWatcher { private CharSequence mBeforeText; - private EasyEditSpanController mEasyEditSpanController; - - private ChangeWatcher() { - mEasyEditSpanController = new EasyEditSpanController(); - } - public void beforeTextChanged(CharSequence buffer, int start, int before, int after) { if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start @@ -9547,14 +8247,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener TextView.this.sendBeforeTextChanged(buffer, start, before, after); } - public void onTextChanged(CharSequence buffer, int start, - int before, int after) { + public void onTextChanged(CharSequence buffer, int start, int before, int after) { if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); TextView.this.handleTextChanged(buffer, start, before, after); - mEasyEditSpanController.onTextChange(buffer); - if (AccessibilityManager.getInstance(mContext).isEnabled() && (isFocused() || isSelected() && isShown())) { sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after); @@ -9571,8 +8268,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - public void onSpanChanged(Spannable buf, - Object what, int s, int e, int st, int en) { + public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) { if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e + " st=" + st + " en=" + en + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, st, e, en); @@ -9589,2264 +8285,5 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, -1, e, -1); } - - private void hideControllers() { - mEasyEditSpanController.hide(); - } - } - - private static class Blink extends Handler implements Runnable { - private final WeakReference<TextView> mView; - private boolean mCancelled; - - public Blink(TextView v) { - mView = new WeakReference<TextView>(v); - } - - public void run() { - if (mCancelled) { - return; - } - - removeCallbacks(Blink.this); - - TextView tv = mView.get(); - - if (tv != null && tv.shouldBlink()) { - if (tv.mLayout != null) { - tv.invalidateCursorPath(); - } - - postAtTime(this, SystemClock.uptimeMillis() + BLINK); - } - } - - void cancel() { - if (!mCancelled) { - removeCallbacks(Blink.this); - mCancelled = true; - } - } - - void uncancel() { - mCancelled = false; - } - } - - private static class DragLocalState { - public TextView sourceTextView; - public int start, end; - - public DragLocalState(TextView sourceTextView, int start, int end) { - this.sourceTextView = sourceTextView; - this.start = start; - this.end = end; - } - } - - private class PositionListener implements ViewTreeObserver.OnPreDrawListener { - // 3 handles - // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) - private final int MAXIMUM_NUMBER_OF_LISTENERS = 6; - private TextViewPositionListener[] mPositionListeners = - new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; - private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; - private boolean mPositionHasChanged = true; - // Absolute position of the TextView with respect to its parent window - private int mPositionX, mPositionY; - private int mNumberOfListeners; - private boolean mScrollHasChanged; - final int[] mTempCoords = new int[2]; - - public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) { - if (mNumberOfListeners == 0) { - updatePosition(); - ViewTreeObserver vto = TextView.this.getViewTreeObserver(); - vto.addOnPreDrawListener(this); - } - - int emptySlotIndex = -1; - for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { - TextViewPositionListener listener = mPositionListeners[i]; - if (listener == positionListener) { - return; - } else if (emptySlotIndex < 0 && listener == null) { - emptySlotIndex = i; - } - } - - mPositionListeners[emptySlotIndex] = positionListener; - mCanMove[emptySlotIndex] = canMove; - mNumberOfListeners++; - } - - public void removeSubscriber(TextViewPositionListener positionListener) { - for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { - if (mPositionListeners[i] == positionListener) { - mPositionListeners[i] = null; - mNumberOfListeners--; - break; - } - } - - if (mNumberOfListeners == 0) { - ViewTreeObserver vto = TextView.this.getViewTreeObserver(); - vto.removeOnPreDrawListener(this); - } - } - - public int getPositionX() { - return mPositionX; - } - - public int getPositionY() { - return mPositionY; - } - - @Override - public boolean onPreDraw() { - updatePosition(); - - for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { - if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) { - TextViewPositionListener positionListener = mPositionListeners[i]; - if (positionListener != null) { - positionListener.updatePosition(mPositionX, mPositionY, - mPositionHasChanged, mScrollHasChanged); - } - } - } - - mScrollHasChanged = false; - return true; - } - - private void updatePosition() { - TextView.this.getLocationInWindow(mTempCoords); - - mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY; - - mPositionX = mTempCoords[0]; - mPositionY = mTempCoords[1]; - } - - public void onScrollChanged() { - mScrollHasChanged = true; - } - } - - private abstract class PinnedPopupWindow implements TextViewPositionListener { - protected PopupWindow mPopupWindow; - protected ViewGroup mContentView; - int mPositionX, mPositionY; - - protected abstract void createPopupWindow(); - protected abstract void initContentView(); - protected abstract int getTextOffset(); - protected abstract int getVerticalLocalPosition(int line); - protected abstract int clipVertically(int positionY); - - public PinnedPopupWindow() { - createPopupWindow(); - - mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); - mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); - mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); - - initContentView(); - - LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - mContentView.setLayoutParams(wrapContent); - - mPopupWindow.setContentView(mContentView); - } - - public void show() { - TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */); - - computeLocalPosition(); - - final PositionListener positionListener = TextView.this.getPositionListener(); - updatePosition(positionListener.getPositionX(), positionListener.getPositionY()); - } - - protected void measureContent() { - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - mContentView.measure( - View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, - View.MeasureSpec.AT_MOST), - View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, - View.MeasureSpec.AT_MOST)); - } - - /* The popup window will be horizontally centered on the getTextOffset() and vertically - * positioned according to viewportToContentHorizontalOffset. - * - * This method assumes that mContentView has properly been measured from its content. */ - private void computeLocalPosition() { - measureContent(); - final int width = mContentView.getMeasuredWidth(); - final int offset = getTextOffset(); - mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f); - mPositionX += viewportToContentHorizontalOffset(); - - final int line = mLayout.getLineForOffset(offset); - mPositionY = getVerticalLocalPosition(line); - mPositionY += viewportToContentVerticalOffset(); - } - - private void updatePosition(int parentPositionX, int parentPositionY) { - int positionX = parentPositionX + mPositionX; - int positionY = parentPositionY + mPositionY; - - positionY = clipVertically(positionY); - - // Horizontal clipping - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - final int width = mContentView.getMeasuredWidth(); - positionX = Math.min(displayMetrics.widthPixels - width, positionX); - positionX = Math.max(0, positionX); - - if (isShowing()) { - mPopupWindow.update(positionX, positionY, -1, -1); - } else { - mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY, - positionX, positionY); - } - } - - public void hide() { - mPopupWindow.dismiss(); - TextView.this.getPositionListener().removeSubscriber(this); - } - - @Override - public void updatePosition(int parentPositionX, int parentPositionY, - boolean parentPositionChanged, boolean parentScrolled) { - // Either parentPositionChanged or parentScrolled is true, check if still visible - if (isShowing() && isOffsetVisible(getTextOffset())) { - if (parentScrolled) computeLocalPosition(); - updatePosition(parentPositionX, parentPositionY); - } else { - hide(); - } - } - - public boolean isShowing() { - return mPopupWindow.isShowing(); - } - } - - private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener { - private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; - private static final int ADD_TO_DICTIONARY = -1; - private static final int DELETE_TEXT = -2; - private SuggestionInfo[] mSuggestionInfos; - private int mNumberOfSuggestions; - private boolean mCursorWasVisibleBeforeSuggestions; - private boolean mIsShowingUp = false; - private SuggestionAdapter mSuggestionsAdapter; - private final Comparator<SuggestionSpan> mSuggestionSpanComparator; - private final HashMap<SuggestionSpan, Integer> mSpansLengths; - - private class CustomPopupWindow extends PopupWindow { - public CustomPopupWindow(Context context, int defStyle) { - super(context, null, defStyle); - } - - @Override - public void dismiss() { - super.dismiss(); - - TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this); - - // Safe cast since show() checks that mText is an Editable - ((Spannable) mText).removeSpan(getEditor().mSuggestionRangeSpan); - - setCursorVisible(mCursorWasVisibleBeforeSuggestions); - if (hasInsertionController()) { - getInsertionController().show(); - } - } - } - - public SuggestionsPopupWindow() { - mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible; - mSuggestionSpanComparator = new SuggestionSpanComparator(); - mSpansLengths = new HashMap<SuggestionSpan, Integer>(); - } - - @Override - protected void createPopupWindow() { - mPopupWindow = new CustomPopupWindow(TextView.this.mContext, - com.android.internal.R.attr.textSuggestionsWindowStyle); - mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - mPopupWindow.setFocusable(true); - mPopupWindow.setClippingEnabled(false); - } - - @Override - protected void initContentView() { - ListView listView = new ListView(TextView.this.getContext()); - mSuggestionsAdapter = new SuggestionAdapter(); - listView.setAdapter(mSuggestionsAdapter); - listView.setOnItemClickListener(this); - mContentView = listView; - - // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete - mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2]; - for (int i = 0; i < mSuggestionInfos.length; i++) { - mSuggestionInfos[i] = new SuggestionInfo(); - } - } - - public boolean isShowingUp() { - return mIsShowingUp; - } - - public void onParentLostFocus() { - mIsShowingUp = false; - } - - private class SuggestionInfo { - int suggestionStart, suggestionEnd; // range of actual suggestion within text - SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents - int suggestionIndex; // the index of this suggestion inside suggestionSpan - SpannableStringBuilder text = new SpannableStringBuilder(); - TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext, - android.R.style.TextAppearance_SuggestionHighlight); - } - - private class SuggestionAdapter extends BaseAdapter { - private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext. - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - @Override - public int getCount() { - return mNumberOfSuggestions; - } - - @Override - public Object getItem(int position) { - return mSuggestionInfos[position]; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - TextView textView = (TextView) convertView; - - if (textView == null) { - textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent, - false); - } - - final SuggestionInfo suggestionInfo = mSuggestionInfos[position]; - textView.setText(suggestionInfo.text); - - if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { - textView.setCompoundDrawablesWithIntrinsicBounds( - com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0); - } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) { - textView.setCompoundDrawablesWithIntrinsicBounds( - com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0); - } else { - textView.setCompoundDrawables(null, null, null, null); - } - - return textView; - } - } - - private class SuggestionSpanComparator implements Comparator<SuggestionSpan> { - public int compare(SuggestionSpan span1, SuggestionSpan span2) { - final int flag1 = span1.getFlags(); - final int flag2 = span2.getFlags(); - if (flag1 != flag2) { - // The order here should match what is used in updateDrawState - final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; - final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; - final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0; - final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0; - if (easy1 && !misspelled1) return -1; - if (easy2 && !misspelled2) return 1; - if (misspelled1) return -1; - if (misspelled2) return 1; - } - - return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue(); - } - } - - /** - * Returns the suggestion spans that cover the current cursor position. The suggestion - * spans are sorted according to the length of text that they are attached to. - */ - private SuggestionSpan[] getSuggestionSpans() { - int pos = TextView.this.getSelectionStart(); - Spannable spannable = (Spannable) TextView.this.mText; - SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); - - mSpansLengths.clear(); - for (SuggestionSpan suggestionSpan : suggestionSpans) { - int start = spannable.getSpanStart(suggestionSpan); - int end = spannable.getSpanEnd(suggestionSpan); - mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start)); - } - - // The suggestions are sorted according to their types (easy correction first, then - // misspelled) and to the length of the text that they cover (shorter first). - Arrays.sort(suggestionSpans, mSuggestionSpanComparator); - return suggestionSpans; - } - - @Override - public void show() { - if (!(mText instanceof Editable)) return; - - if (updateSuggestions()) { - mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible; - setCursorVisible(false); - mIsShowingUp = true; - super.show(); - } - } - - @Override - protected void measureContent() { - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec( - displayMetrics.widthPixels, View.MeasureSpec.AT_MOST); - final int verticalMeasure = View.MeasureSpec.makeMeasureSpec( - displayMetrics.heightPixels, View.MeasureSpec.AT_MOST); - - int width = 0; - View view = null; - for (int i = 0; i < mNumberOfSuggestions; i++) { - view = mSuggestionsAdapter.getView(i, view, mContentView); - view.getLayoutParams().width = LayoutParams.WRAP_CONTENT; - view.measure(horizontalMeasure, verticalMeasure); - width = Math.max(width, view.getMeasuredWidth()); - } - - // Enforce the width based on actual text widths - mContentView.measure( - View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), - verticalMeasure); - - Drawable popupBackground = mPopupWindow.getBackground(); - if (popupBackground != null) { - if (mTempRect == null) mTempRect = new Rect(); - popupBackground.getPadding(mTempRect); - width += mTempRect.left + mTempRect.right; - } - mPopupWindow.setWidth(width); - } - - @Override - protected int getTextOffset() { - return getSelectionStart(); - } - - @Override - protected int getVerticalLocalPosition(int line) { - return mLayout.getLineBottom(line); - } - - @Override - protected int clipVertically(int positionY) { - final int height = mContentView.getMeasuredHeight(); - final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - return Math.min(positionY, displayMetrics.heightPixels - height); - } - - @Override - public void hide() { - super.hide(); - } - - private boolean updateSuggestions() { - Spannable spannable = (Spannable) TextView.this.mText; - SuggestionSpan[] suggestionSpans = getSuggestionSpans(); - - final int nbSpans = suggestionSpans.length; - // Suggestions are shown after a delay: the underlying spans may have been removed - if (nbSpans == 0) return false; - - mNumberOfSuggestions = 0; - int spanUnionStart = mText.length(); - int spanUnionEnd = 0; - - SuggestionSpan misspelledSpan = null; - int underlineColor = 0; - - for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { - SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; - final int spanStart = spannable.getSpanStart(suggestionSpan); - final int spanEnd = spannable.getSpanEnd(suggestionSpan); - spanUnionStart = Math.min(spanStart, spanUnionStart); - spanUnionEnd = Math.max(spanEnd, spanUnionEnd); - - if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) { - misspelledSpan = suggestionSpan; - } - - // The first span dictates the background color of the highlighted text - if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor(); - - String[] suggestions = suggestionSpan.getSuggestions(); - int nbSuggestions = suggestions.length; - for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { - String suggestion = suggestions[suggestionIndex]; - - boolean suggestionIsDuplicate = false; - for (int i = 0; i < mNumberOfSuggestions; i++) { - if (mSuggestionInfos[i].text.toString().equals(suggestion)) { - SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan; - final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan); - final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan); - if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) { - suggestionIsDuplicate = true; - break; - } - } - } - - if (!suggestionIsDuplicate) { - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = suggestionSpan; - suggestionInfo.suggestionIndex = suggestionIndex; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion); - - mNumberOfSuggestions++; - - if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) { - // Also end outer for loop - spanIndex = nbSpans; - break; - } - } - } - } - - for (int i = 0; i < mNumberOfSuggestions; i++) { - highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); - } - - // Add "Add to dictionary" item if there is a span with the misspelled flag - if (misspelledSpan != null) { - final int misspelledStart = spannable.getSpanStart(misspelledSpan); - final int misspelledEnd = spannable.getSpanEnd(misspelledSpan); - if (misspelledStart >= 0 && misspelledEnd > misspelledStart) { - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = misspelledSpan; - suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), - getContext().getString(com.android.internal.R.string.addToDictionary)); - suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - mNumberOfSuggestions++; - } - } - - // Delete item - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = null; - suggestionInfo.suggestionIndex = DELETE_TEXT; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), - getContext().getString(com.android.internal.R.string.deleteText)); - suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - mNumberOfSuggestions++; - - if (getEditor().mSuggestionRangeSpan == null) getEditor().mSuggestionRangeSpan = new SuggestionRangeSpan(); - if (underlineColor == 0) { - // Fallback on the default highlight color when the first span does not provide one - getEditor().mSuggestionRangeSpan.setBackgroundColor(mHighlightColor); - } else { - final float BACKGROUND_TRANSPARENCY = 0.4f; - final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY); - getEditor().mSuggestionRangeSpan.setBackgroundColor( - (underlineColor & 0x00FFFFFF) + (newAlpha << 24)); - } - spannable.setSpan(getEditor().mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - mSuggestionsAdapter.notifyDataSetChanged(); - return true; - } - - private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, - int unionEnd) { - final Spannable text = (Spannable) mText; - final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan); - final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan); - - // Adjust the start/end of the suggestion span - suggestionInfo.suggestionStart = spanStart - unionStart; - suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart - + suggestionInfo.text.length(); - - suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, - suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - // Add the text before and after the span. - final String textAsString = text.toString(); - suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart)); - suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd)); - } - - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - Editable editable = (Editable) mText; - SuggestionInfo suggestionInfo = mSuggestionInfos[position]; - - if (suggestionInfo.suggestionIndex == DELETE_TEXT) { - final int spanUnionStart = editable.getSpanStart(getEditor().mSuggestionRangeSpan); - int spanUnionEnd = editable.getSpanEnd(getEditor().mSuggestionRangeSpan); - if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { - // Do not leave two adjacent spaces after deletion, or one at beginning of text - if (spanUnionEnd < editable.length() && - Character.isSpaceChar(editable.charAt(spanUnionEnd)) && - (spanUnionStart == 0 || - Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) { - spanUnionEnd = spanUnionEnd + 1; - } - deleteText_internal(spanUnionStart, spanUnionEnd); - } - hide(); - return; - } - - final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan); - final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan); - if (spanStart < 0 || spanEnd <= spanStart) { - // Span has been removed - hide(); - return; - } - final String originalText = mText.toString().substring(spanStart, spanEnd); - - if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { - Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); - intent.putExtra("word", originalText); - intent.putExtra("locale", getTextServicesLocale().toString()); - intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); - getContext().startActivity(intent); - // There is no way to know if the word was indeed added. Re-check. - // TODO The ExtractEditText should remove the span in the original text instead - editable.removeSpan(suggestionInfo.suggestionSpan); - updateSpellCheckSpans(spanStart, spanEnd, false); - } else { - // SuggestionSpans are removed by replace: save them before - SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, - SuggestionSpan.class); - final int length = suggestionSpans.length; - int[] suggestionSpansStarts = new int[length]; - int[] suggestionSpansEnds = new int[length]; - int[] suggestionSpansFlags = new int[length]; - for (int i = 0; i < length; i++) { - final SuggestionSpan suggestionSpan = suggestionSpans[i]; - suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); - suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); - suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); - - // Remove potential misspelled flags - int suggestionSpanFlags = suggestionSpan.getFlags(); - if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) { - suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; - suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; - suggestionSpan.setFlags(suggestionSpanFlags); - } - } - - final int suggestionStart = suggestionInfo.suggestionStart; - final int suggestionEnd = suggestionInfo.suggestionEnd; - final String suggestion = suggestionInfo.text.subSequence( - suggestionStart, suggestionEnd).toString(); - replaceText_internal(spanStart, spanEnd, suggestion); - - // Notify source IME of the suggestion pick. Do this before swaping texts. - if (!TextUtils.isEmpty( - suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText, - suggestionInfo.suggestionIndex); - } - } - - // Swap text content between actual text and Suggestion span - String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); - suggestions[suggestionInfo.suggestionIndex] = originalText; - - // Restore previous SuggestionSpans - final int lengthDifference = suggestion.length() - (spanEnd - spanStart); - for (int i = 0; i < length; i++) { - // Only spans that include the modified region make sense after replacement - // Spans partially included in the replaced region are removed, there is no - // way to assign them a valid range after replacement - if (suggestionSpansStarts[i] <= spanStart && - suggestionSpansEnds[i] >= spanEnd) { - setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], - suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]); - } - } - - // Move cursor at the end of the replaced word - final int newCursorPosition = spanEnd + lengthDifference; - setCursorPosition_internal(newCursorPosition, newCursorPosition); - } - - hide(); - } - } - - /** - * An ActionMode Callback class that is used to provide actions while in text selection mode. - * - * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending - * on which of these this TextView supports. - */ - private class SelectionActionModeCallback implements ActionMode.Callback { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - TypedArray styledAttributes = mContext.obtainStyledAttributes( - com.android.internal.R.styleable.SelectionModeDrawables); - - boolean allowText = getContext().getResources().getBoolean( - com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon); - - mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle)); - mode.setSubtitle(null); - mode.setTitleOptionalHint(true); - - int selectAllIconId = 0; // No icon by default - if (!allowText) { - // Provide an icon, text will not be displayed on smaller screens. - selectAllIconId = styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0); - } - - menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). - setIcon(selectAllIconId). - setAlphabeticShortcut('a'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - - if (canCut()) { - menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut). - setIcon(styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)). - setAlphabeticShortcut('x'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - - if (canCopy()) { - menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). - setIcon(styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)). - setAlphabeticShortcut('c'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - - if (canPaste()) { - menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). - setIcon(styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)). - setAlphabeticShortcut('v'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - - styledAttributes.recycle(); - - if (getEditor().mCustomSelectionActionModeCallback != null) { - if (!getEditor().mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) { - // The custom mode can choose to cancel the action mode - return false; - } - } - - if (menu.hasVisibleItems() || mode.getCustomView() != null) { - getSelectionController().show(); - return true; - } else { - return false; - } - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - if (getEditor().mCustomSelectionActionModeCallback != null) { - return getEditor().mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu); - } - return true; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (getEditor().mCustomSelectionActionModeCallback != null && - getEditor().mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) { - return true; - } - return onTextContextMenuItem(item.getItemId()); - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - if (getEditor().mCustomSelectionActionModeCallback != null) { - getEditor().mCustomSelectionActionModeCallback.onDestroyActionMode(mode); - } - Selection.setSelection((Spannable) mText, getSelectionEnd()); - - if (getEditor().mSelectionModifierCursorController != null) { - getEditor().mSelectionModifierCursorController.hide(); - } - - getEditor().mSelectionActionMode = null; - } - } - - private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener { - private static final int POPUP_TEXT_LAYOUT = - com.android.internal.R.layout.text_edit_action_popup_text; - private TextView mPasteTextView; - private TextView mReplaceTextView; - - @Override - protected void createPopupWindow() { - mPopupWindow = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mPopupWindow.setClippingEnabled(true); - } - - @Override - protected void initContentView() { - LinearLayout linearLayout = new LinearLayout(TextView.this.getContext()); - linearLayout.setOrientation(LinearLayout.HORIZONTAL); - mContentView = linearLayout; - mContentView.setBackgroundResource( - com.android.internal.R.drawable.text_edit_paste_window); - - LayoutInflater inflater = (LayoutInflater)TextView.this.mContext. - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - LayoutParams wrapContent = new LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mPasteTextView.setLayoutParams(wrapContent); - mContentView.addView(mPasteTextView); - mPasteTextView.setText(com.android.internal.R.string.paste); - mPasteTextView.setOnClickListener(this); - - mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mReplaceTextView.setLayoutParams(wrapContent); - mContentView.addView(mReplaceTextView); - mReplaceTextView.setText(com.android.internal.R.string.replace); - mReplaceTextView.setOnClickListener(this); - } - - @Override - public void show() { - boolean canPaste = canPaste(); - boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan(); - mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE); - mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE); - - if (!canPaste && !canSuggest) return; - - super.show(); - } - - @Override - public void onClick(View view) { - if (view == mPasteTextView && canPaste()) { - onTextContextMenuItem(ID_PASTE); - hide(); - } else if (view == mReplaceTextView) { - final int middle = (getSelectionStart() + getSelectionEnd()) / 2; - stopSelectionActionMode(); - Selection.setSelection((Spannable) mText, middle); - showSuggestions(); - } - } - - @Override - protected int getTextOffset() { - return (getSelectionStart() + getSelectionEnd()) / 2; - } - - @Override - protected int getVerticalLocalPosition(int line) { - return mLayout.getLineTop(line) - mContentView.getMeasuredHeight(); - } - - @Override - protected int clipVertically(int positionY) { - if (positionY < 0) { - final int offset = getTextOffset(); - final int line = mLayout.getLineForOffset(offset); - positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line); - positionY += mContentView.getMeasuredHeight(); - - // Assumes insertion and selection handles share the same height - final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes); - positionY += handle.getIntrinsicHeight(); - } - - return positionY; - } - } - - private abstract class HandleView extends View implements TextViewPositionListener { - protected Drawable mDrawable; - protected Drawable mDrawableLtr; - protected Drawable mDrawableRtl; - private final PopupWindow mContainer; - // Position with respect to the parent TextView - private int mPositionX, mPositionY; - private boolean mIsDragging; - // Offset from touch position to mPosition - private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; - protected int mHotspotX; - // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up - private float mTouchOffsetY; - // Where the touch position should be on the handle to ensure a maximum cursor visibility - private float mIdealVerticalOffset; - // Parent's (TextView) previous position in window - private int mLastParentX, mLastParentY; - // Transient action popup window for Paste and Replace actions - protected ActionPopupWindow mActionPopupWindow; - // Previous text character offset - private int mPreviousOffset = -1; - // Previous text character offset - private boolean mPositionHasChanged = true; - // Used to delay the appearance of the action popup window - private Runnable mActionPopupShower; - - public HandleView(Drawable drawableLtr, Drawable drawableRtl) { - super(TextView.this.mContext); - mContainer = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mContainer.setSplitTouchEnabled(true); - mContainer.setClippingEnabled(false); - mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); - mContainer.setContentView(this); - - mDrawableLtr = drawableLtr; - mDrawableRtl = drawableRtl; - - updateDrawable(); - - final int handleHeight = mDrawable.getIntrinsicHeight(); - mTouchOffsetY = -0.3f * handleHeight; - mIdealVerticalOffset = 0.7f * handleHeight; - } - - protected void updateDrawable() { - final int offset = getCurrentCursorOffset(); - final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset); - mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr; - mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset); - } - - protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun); - - // Touch-up filter: number of previous positions remembered - private static final int HISTORY_SIZE = 5; - private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150; - private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350; - private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE]; - private final int[] mPreviousOffsets = new int[HISTORY_SIZE]; - private int mPreviousOffsetIndex = 0; - private int mNumberPreviousOffsets = 0; - - private void startTouchUpFilter(int offset) { - mNumberPreviousOffsets = 0; - addPositionToTouchUpFilter(offset); - } - - private void addPositionToTouchUpFilter(int offset) { - mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE; - mPreviousOffsets[mPreviousOffsetIndex] = offset; - mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis(); - mNumberPreviousOffsets++; - } - - private void filterOnTouchUp() { - final long now = SystemClock.uptimeMillis(); - int i = 0; - int index = mPreviousOffsetIndex; - final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE); - while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) { - i++; - index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE; - } - - if (i > 0 && i < iMax && - (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { - positionAtCursorOffset(mPreviousOffsets[index], false); - } - } - - public boolean offsetHasBeenChanged() { - return mNumberPreviousOffsets > 1; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); - } - - public void show() { - if (isShowing()) return; - - getPositionListener().addSubscriber(this, true /* local position may change */); - - // Make sure the offset is always considered new, even when focusing at same position - mPreviousOffset = -1; - positionAtCursorOffset(getCurrentCursorOffset(), false); - - hideActionPopupWindow(); - } - - protected void dismiss() { - mIsDragging = false; - mContainer.dismiss(); - onDetached(); - } - - public void hide() { - dismiss(); - - TextView.this.getPositionListener().removeSubscriber(this); - } - - void showActionPopupWindow(int delay) { - if (mActionPopupWindow == null) { - mActionPopupWindow = new ActionPopupWindow(); - } - if (mActionPopupShower == null) { - mActionPopupShower = new Runnable() { - public void run() { - mActionPopupWindow.show(); - } - }; - } else { - TextView.this.removeCallbacks(mActionPopupShower); - } - TextView.this.postDelayed(mActionPopupShower, delay); - } - - protected void hideActionPopupWindow() { - if (mActionPopupShower != null) { - TextView.this.removeCallbacks(mActionPopupShower); - } - if (mActionPopupWindow != null) { - mActionPopupWindow.hide(); - } - } - - public boolean isShowing() { - return mContainer.isShowing(); - } - - private boolean isVisible() { - // Always show a dragging handle. - if (mIsDragging) { - return true; - } - - if (isInBatchEditMode()) { - return false; - } - - return TextView.this.isPositionVisible(mPositionX + mHotspotX, mPositionY); - } - - public abstract int getCurrentCursorOffset(); - - protected abstract void updateSelection(int offset); - - public abstract void updatePosition(float x, float y); - - protected void positionAtCursorOffset(int offset, boolean parentScrolled) { - // A HandleView relies on the layout, which may be nulled by external methods - if (mLayout == null) { - // Will update controllers' state, hiding them and stopping selection mode if needed - prepareCursorControllers(); - return; - } - - boolean offsetChanged = offset != mPreviousOffset; - if (offsetChanged || parentScrolled) { - if (offsetChanged) { - updateSelection(offset); - addPositionToTouchUpFilter(offset); - } - final int line = mLayout.getLineForOffset(offset); - - mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX); - mPositionY = mLayout.getLineBottom(line); - - // Take TextView's padding and scroll into account. - mPositionX += viewportToContentHorizontalOffset(); - mPositionY += viewportToContentVerticalOffset(); - - mPreviousOffset = offset; - mPositionHasChanged = true; - } - } - - public void updatePosition(int parentPositionX, int parentPositionY, - boolean parentPositionChanged, boolean parentScrolled) { - positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled); - if (parentPositionChanged || mPositionHasChanged) { - if (mIsDragging) { - // Update touchToWindow offset in case of parent scrolling while dragging - if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) { - mTouchToWindowOffsetX += parentPositionX - mLastParentX; - mTouchToWindowOffsetY += parentPositionY - mLastParentY; - mLastParentX = parentPositionX; - mLastParentY = parentPositionY; - } - - onHandleMoved(); - } - - if (isVisible()) { - final int positionX = parentPositionX + mPositionX; - final int positionY = parentPositionY + mPositionY; - if (isShowing()) { - mContainer.update(positionX, positionY, -1, -1); - } else { - mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, - positionX, positionY); - } - } else { - if (isShowing()) { - dismiss(); - } - } - - mPositionHasChanged = false; - } - } - - @Override - protected void onDraw(Canvas c) { - mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop); - mDrawable.draw(c); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - startTouchUpFilter(getCurrentCursorOffset()); - mTouchToWindowOffsetX = ev.getRawX() - mPositionX; - mTouchToWindowOffsetY = ev.getRawY() - mPositionY; - - final PositionListener positionListener = getPositionListener(); - mLastParentX = positionListener.getPositionX(); - mLastParentY = positionListener.getPositionY(); - mIsDragging = true; - break; - } - - case MotionEvent.ACTION_MOVE: { - final float rawX = ev.getRawX(); - final float rawY = ev.getRawY(); - - // Vertical hysteresis: vertical down movement tends to snap to ideal offset - final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY; - final float currentVerticalOffset = rawY - mPositionY - mLastParentY; - float newVerticalOffset; - if (previousVerticalOffset < mIdealVerticalOffset) { - newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset); - newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset); - } else { - newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset); - newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset); - } - mTouchToWindowOffsetY = newVerticalOffset + mLastParentY; - - final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; - final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY; - - updatePosition(newPosX, newPosY); - break; - } - - case MotionEvent.ACTION_UP: - filterOnTouchUp(); - mIsDragging = false; - break; - - case MotionEvent.ACTION_CANCEL: - mIsDragging = false; - break; - } - return true; - } - - public boolean isDragging() { - return mIsDragging; - } - - void onHandleMoved() { - hideActionPopupWindow(); - } - - public void onDetached() { - hideActionPopupWindow(); - } - } - - private class InsertionHandleView extends HandleView { - private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; - private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds - - // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow - private float mDownPositionX, mDownPositionY; - private Runnable mHider; - - public InsertionHandleView(Drawable drawable) { - super(drawable, drawable); - } - - @Override - public void show() { - super.show(); - - final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - LAST_CUT_OR_COPY_TIME; - if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { - showActionPopupWindow(0); - } - - hideAfterDelay(); - } - - public void showWithActionPopup() { - show(); - showActionPopupWindow(0); - } - - private void hideAfterDelay() { - if (mHider == null) { - mHider = new Runnable() { - public void run() { - hide(); - } - }; - } else { - removeHiderCallback(); - } - TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT); - } - - private void removeHiderCallback() { - if (mHider != null) { - TextView.this.removeCallbacks(mHider); - } - } - - @Override - protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - return drawable.getIntrinsicWidth() / 2; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - final boolean result = super.onTouchEvent(ev); - - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mDownPositionX = ev.getRawX(); - mDownPositionY = ev.getRawY(); - break; - - case MotionEvent.ACTION_UP: - if (!offsetHasBeenChanged()) { - final float deltaX = mDownPositionX - ev.getRawX(); - final float deltaY = mDownPositionY - ev.getRawY(); - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - final ViewConfiguration viewConfiguration = ViewConfiguration.get( - TextView.this.getContext()); - final int touchSlop = viewConfiguration.getScaledTouchSlop(); - - if (distanceSquared < touchSlop * touchSlop) { - if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) { - // Tapping on the handle dismisses the displayed action popup - mActionPopupWindow.hide(); - } else { - showWithActionPopup(); - } - } - } - hideAfterDelay(); - break; - - case MotionEvent.ACTION_CANCEL: - hideAfterDelay(); - break; - - default: - break; - } - - return result; - } - - @Override - public int getCurrentCursorOffset() { - return TextView.this.getSelectionStart(); - } - - @Override - public void updateSelection(int offset) { - Selection.setSelection((Spannable) mText, offset); - } - - @Override - public void updatePosition(float x, float y) { - positionAtCursorOffset(getOffsetForPosition(x, y), false); - } - - @Override - void onHandleMoved() { - super.onHandleMoved(); - removeHiderCallback(); - } - - @Override - public void onDetached() { - super.onDetached(); - removeHiderCallback(); - } - } - - private class SelectionStartHandleView extends HandleView { - - public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) { - super(drawableLtr, drawableRtl); - } - - @Override - protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return drawable.getIntrinsicWidth() / 4; - } else { - return (drawable.getIntrinsicWidth() * 3) / 4; - } - } - - @Override - public int getCurrentCursorOffset() { - return TextView.this.getSelectionStart(); - } - - @Override - public void updateSelection(int offset) { - Selection.setSelection((Spannable) mText, offset, getSelectionEnd()); - updateDrawable(); - } - - @Override - public void updatePosition(float x, float y) { - int offset = getOffsetForPosition(x, y); - - // Handles can not cross and selection is at least one character - final int selectionEnd = getSelectionEnd(); - if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1); - - positionAtCursorOffset(offset, false); - } - - public ActionPopupWindow getActionPopupWindow() { - return mActionPopupWindow; - } - } - - private class SelectionEndHandleView extends HandleView { - - public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) { - super(drawableLtr, drawableRtl); - } - - @Override - protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return (drawable.getIntrinsicWidth() * 3) / 4; - } else { - return drawable.getIntrinsicWidth() / 4; - } - } - - @Override - public int getCurrentCursorOffset() { - return TextView.this.getSelectionEnd(); - } - - @Override - public void updateSelection(int offset) { - Selection.setSelection((Spannable) mText, getSelectionStart(), offset); - updateDrawable(); - } - - @Override - public void updatePosition(float x, float y) { - int offset = getOffsetForPosition(x, y); - - // Handles can not cross and selection is at least one character - final int selectionStart = getSelectionStart(); - if (offset <= selectionStart) offset = Math.min(selectionStart + 1, mText.length()); - - positionAtCursorOffset(offset, false); - } - - public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) { - mActionPopupWindow = actionPopupWindow; - } - } - - /** - * A CursorController instance can be used to control a cursor in the text. - * It is not used outside of {@link TextView}. - * @hide - */ - private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { - /** - * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. - * See also {@link #hide()}. - */ - public void show(); - - /** - * Hide the cursor controller from screen. - * See also {@link #show()}. - */ - public void hide(); - - /** - * Called when the view is detached from window. Perform house keeping task, such as - * stopping Runnable thread that would otherwise keep a reference on the context, thus - * preventing the activity from being recycled. - */ - public void onDetached(); - } - - private class InsertionPointCursorController implements CursorController { - private InsertionHandleView mHandle; - - public void show() { - getHandle().show(); - } - - public void showWithActionPopup() { - getHandle().showWithActionPopup(); - } - - public void hide() { - if (mHandle != null) { - mHandle.hide(); - } - } - - public void onTouchModeChanged(boolean isInTouchMode) { - if (!isInTouchMode) { - hide(); - } - } - - private InsertionHandleView getHandle() { - if (getEditor().mSelectHandleCenter == null) { - getEditor().mSelectHandleCenter = mContext.getResources().getDrawable( - mTextSelectHandleRes); - } - if (mHandle == null) { - mHandle = new InsertionHandleView(getEditor().mSelectHandleCenter); - } - return mHandle; - } - - @Override - public void onDetached() { - final ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnTouchModeChangeListener(this); - - if (mHandle != null) mHandle.onDetached(); - } - } - - private class SelectionModifierCursorController implements CursorController { - private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds - // The cursor controller handles, lazily created when shown. - private SelectionStartHandleView mStartHandle; - private SelectionEndHandleView mEndHandle; - // The offsets of that last touch down event. Remembered to start selection there. - private int mMinTouchOffset, mMaxTouchOffset; - - // Double tap detection - private long mPreviousTapUpTime = 0; - private float mDownPositionX, mDownPositionY; - private boolean mGestureStayedInTapRegion; - - SelectionModifierCursorController() { - resetTouchOffsets(); - } - - public void show() { - if (isInBatchEditMode()) { - return; - } - initDrawables(); - initHandles(); - hideInsertionPointCursorController(); - } - - private void initDrawables() { - if (getEditor().mSelectHandleLeft == null) { - getEditor().mSelectHandleLeft = mContext.getResources().getDrawable( - mTextSelectHandleLeftRes); - } - if (getEditor().mSelectHandleRight == null) { - getEditor().mSelectHandleRight = mContext.getResources().getDrawable( - mTextSelectHandleRightRes); - } - } - - private void initHandles() { - // Lazy object creation has to be done before updatePosition() is called. - if (mStartHandle == null) { - mStartHandle = new SelectionStartHandleView(getEditor().mSelectHandleLeft, getEditor().mSelectHandleRight); - } - if (mEndHandle == null) { - mEndHandle = new SelectionEndHandleView(getEditor().mSelectHandleRight, getEditor().mSelectHandleLeft); - } - - mStartHandle.show(); - mEndHandle.show(); - - // Make sure both left and right handles share the same ActionPopupWindow (so that - // moving any of the handles hides the action popup). - mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION); - mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow()); - - hideInsertionPointCursorController(); - } - - public void hide() { - if (mStartHandle != null) mStartHandle.hide(); - if (mEndHandle != null) mEndHandle.hide(); - } - - public void onTouchEvent(MotionEvent event) { - // This is done even when the View does not have focus, so that long presses can start - // selection and tap can move cursor from this tap position. - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - final float x = event.getX(); - final float y = event.getY(); - - // Remember finger down position, to be able to start selection from there - mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); - - // Double tap detection - if (mGestureStayedInTapRegion) { - long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; - if (duration <= ViewConfiguration.getDoubleTapTimeout()) { - final float deltaX = x - mDownPositionX; - final float deltaY = y - mDownPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - ViewConfiguration viewConfiguration = ViewConfiguration.get( - TextView.this.getContext()); - int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); - boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop; - - if (stayedInArea && isPositionOnText(x, y)) { - startSelectionActionMode(); - getEditor().mDiscardNextActionUp = true; - } - } - } - - mDownPositionX = x; - mDownPositionY = y; - mGestureStayedInTapRegion = true; - break; - - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: - // Handle multi-point gestures. Keep min and max offset positions. - // Only activated for devices that correctly handle multi-touch. - if (mContext.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { - updateMinAndMaxOffsets(event); - } - break; - - case MotionEvent.ACTION_MOVE: - if (mGestureStayedInTapRegion) { - final float deltaX = event.getX() - mDownPositionX; - final float deltaY = event.getY() - mDownPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - final ViewConfiguration viewConfiguration = ViewConfiguration.get( - TextView.this.getContext()); - int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); - - if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { - mGestureStayedInTapRegion = false; - } - } - break; - - case MotionEvent.ACTION_UP: - mPreviousTapUpTime = SystemClock.uptimeMillis(); - break; - } - } - - /** - * @param event - */ - private void updateMinAndMaxOffsets(MotionEvent event) { - int pointerCount = event.getPointerCount(); - for (int index = 0; index < pointerCount; index++) { - int offset = getOffsetForPosition(event.getX(index), event.getY(index)); - if (offset < mMinTouchOffset) mMinTouchOffset = offset; - if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; - } - } - - public int getMinTouchOffset() { - return mMinTouchOffset; - } - - public int getMaxTouchOffset() { - return mMaxTouchOffset; - } - - public void resetTouchOffsets() { - mMinTouchOffset = mMaxTouchOffset = -1; - } - - /** - * @return true iff this controller is currently used to move the selection start. - */ - public boolean isSelectionStartDragged() { - return mStartHandle != null && mStartHandle.isDragging(); - } - - public void onTouchModeChanged(boolean isInTouchMode) { - if (!isInTouchMode) { - hide(); - } - } - - @Override - public void onDetached() { - final ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnTouchModeChangeListener(this); - - if (mStartHandle != null) mStartHandle.onDetached(); - if (mEndHandle != null) mEndHandle.onDetached(); - } - } - - static class InputContentType { - int imeOptions = EditorInfo.IME_NULL; - String privateImeOptions; - CharSequence imeActionLabel; - int imeActionId; - Bundle extras; - OnEditorActionListener onEditorActionListener; - boolean enterDown; - } - - static class InputMethodState { - Rect mCursorRectInWindow = new Rect(); - RectF mTmpRectF = new RectF(); - float[] mTmpOffset = new float[2]; - ExtractedTextRequest mExtracting; - final ExtractedText mTmpExtracted = new ExtractedText(); - int mBatchEditNesting; - boolean mCursorChanged; - boolean mSelectionModeChanged; - boolean mContentChanged; - int mChangedStart, mChangedEnd, mChangedDelta; - } - - private class Editor { - // Cursor Controllers. - InsertionPointCursorController mInsertionPointCursorController; - SelectionModifierCursorController mSelectionModifierCursorController; - ActionMode mSelectionActionMode; - boolean mInsertionControllerEnabled; - boolean mSelectionControllerEnabled; - - // Used to highlight a word when it is corrected by the IME - CorrectionHighlighter mCorrectionHighlighter; - - InputContentType mInputContentType; - InputMethodState mInputMethodState; - - DisplayList[] mTextDisplayLists; - - boolean mFrozenWithFocus; - boolean mSelectionMoved; - boolean mTouchFocusSelected; - - KeyListener mKeyListener; - int mInputType = EditorInfo.TYPE_NULL; - - boolean mDiscardNextActionUp; - boolean mIgnoreActionUpEvent; - - long mShowCursor; - Blink mBlink; - - boolean mCursorVisible = true; - boolean mSelectAllOnFocus; - boolean mTextIsSelectable; - - CharSequence mError; - boolean mErrorWasChanged; - ErrorPopup mErrorPopup; - /** - * This flag is set if the TextView tries to display an error before it - * is attached to the window (so its position is still unknown). - * It causes the error to be shown later, when onAttachedToWindow() - * is called. - */ - boolean mShowErrorAfterAttach; - - boolean mInBatchEditControllers; - - SuggestionsPopupWindow mSuggestionsPopupWindow; - SuggestionRangeSpan mSuggestionRangeSpan; - Runnable mShowSuggestionRunnable; - - final Drawable[] mCursorDrawable = new Drawable[2]; - int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split) - - Drawable mSelectHandleLeft; - Drawable mSelectHandleRight; - Drawable mSelectHandleCenter; - - // Global listener that detects changes in the global position of the TextView - PositionListener mPositionListener; - - float mLastDownPositionX, mLastDownPositionY; - Callback mCustomSelectionActionModeCallback; - - // Set when this TextView gained focus with some text selected. Will start selection mode. - boolean mCreatedWithASelection; - - WordIterator mWordIterator; - SpellChecker mSpellChecker; - - void onAttachedToWindow() { - final ViewTreeObserver observer = getViewTreeObserver(); - // No need to create the controller. - // The get method will add the listener on controller creation. - if (mInsertionPointCursorController != null) { - observer.addOnTouchModeChangeListener(mInsertionPointCursorController); - } - if (mSelectionModifierCursorController != null) { - observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); - } - updateSpellCheckSpans(0, mText.length(), true /* create the spell checker if needed */); - } - - void onDetachedFromWindow() { - if (mError != null) { - hideError(); - } - - if (mBlink != null) { - mBlink.removeCallbacks(mBlink); - } - - if (mInsertionPointCursorController != null) { - mInsertionPointCursorController.onDetached(); - } - - if (mSelectionModifierCursorController != null) { - mSelectionModifierCursorController.onDetached(); - } - - if (mShowSuggestionRunnable != null) { - removeCallbacks(mShowSuggestionRunnable); - } - - invalidateTextDisplayList(); - - if (mSpellChecker != null) { - mSpellChecker.closeSession(); - // Forces the creation of a new SpellChecker next time this window is created. - // Will handle the cases where the settings has been changed in the meantime. - mSpellChecker = null; - } - - hideControllers(); - } - - void onScreenStateChanged(int screenState) { - switch (screenState) { - case SCREEN_STATE_ON: - resumeBlink(); - break; - case SCREEN_STATE_OFF: - suspendBlink(); - break; - } - } - - private void suspendBlink() { - if (mBlink != null) { - mBlink.cancel(); - } - } - - private void resumeBlink() { - if (mBlink != null) { - mBlink.uncancel(); - makeBlink(); - } - } - - void adjustInputType(boolean password, boolean passwordInputType, - boolean webPasswordInputType, boolean numberPasswordInputType) { - // mInputType has been set from inputType, possibly modified by mInputMethod. - // Specialize mInputType to [web]password if we have a text class and the original input - // type was a password. - if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { - if (password || passwordInputType) { - mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) - | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; - } - if (webPasswordInputType) { - mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) - | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; - } - } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) { - if (numberPasswordInputType) { - mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) - | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD; - } - } - } - - void setFrame() { - if (mErrorPopup != null) { - TextView tv = (TextView) mErrorPopup.getContentView(); - chooseSize(mErrorPopup, mError, tv); - mErrorPopup.update(TextView.this, getErrorX(), getErrorY(), - mErrorPopup.getWidth(), mErrorPopup.getHeight()); - } - } - - void onFocusChanged(boolean focused, int direction) { - mShowCursor = SystemClock.uptimeMillis(); - ensureEndedBatchEdit(); - - if (focused) { - int selStart = getSelectionStart(); - int selEnd = getSelectionEnd(); - - // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection - // mode for these, unless there was a specific selection already started. - final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 && - selEnd == mText.length(); - - mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted; - - if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { - // If a tap was used to give focus to that view, move cursor at tap position. - // Has to be done before onTakeFocus, which can be overloaded. - final int lastTapPosition = getLastTapPosition(); - if (lastTapPosition >= 0) { - Selection.setSelection((Spannable) mText, lastTapPosition); - } - - // Note this may have to be moved out of the Editor class - if (mMovement != null) { - mMovement.onTakeFocus(TextView.this, (Spannable) mText, direction); - } - - // The DecorView does not have focus when the 'Done' ExtractEditText button is - // pressed. Since it is the ViewAncestor's mView, it requests focus before - // ExtractEditText clears focus, which gives focus to the ExtractEditText. - // This special case ensure that we keep current selection in that case. - // It would be better to know why the DecorView does not have focus at that time. - if (((TextView.this instanceof ExtractEditText) || mSelectionMoved) && - selStart >= 0 && selEnd >= 0) { - /* - * Someone intentionally set the selection, so let them - * do whatever it is that they wanted to do instead of - * the default on-focus behavior. We reset the selection - * here instead of just skipping the onTakeFocus() call - * because some movement methods do something other than - * just setting the selection in theirs and we still - * need to go through that path. - */ - Selection.setSelection((Spannable) mText, selStart, selEnd); - } - - if (mSelectAllOnFocus) { - selectAll(); - } - - mTouchFocusSelected = true; - } - - mFrozenWithFocus = false; - mSelectionMoved = false; - - if (mError != null) { - showError(); - } - - makeBlink(); - } else { - if (mError != null) { - hideError(); - } - // Don't leave us in the middle of a batch edit. - onEndBatchEdit(); - - if (TextView.this instanceof ExtractEditText) { - // terminateTextSelectionMode removes selection, which we want to keep when - // ExtractEditText goes out of focus. - final int selStart = getSelectionStart(); - final int selEnd = getSelectionEnd(); - hideControllers(); - Selection.setSelection((Spannable) mText, selStart, selEnd); - } else { - hideControllers(); - downgradeEasyCorrectionSpans(); - } - - // No need to create the controller - if (mSelectionModifierCursorController != null) { - mSelectionModifierCursorController.resetTouchOffsets(); - } - } - } - - void sendOnTextChanged(int start, int after) { - updateSpellCheckSpans(start, start + after, false); - - // Hide the controllers as soon as text is modified (typing, procedural...) - // We do not hide the span controllers, since they can be added when a new text is - // inserted into the text view (voice IME). - hideCursorControllers(); - } - - private int getLastTapPosition() { - // No need to create the controller at that point, no last tap position saved - if (mSelectionModifierCursorController != null) { - int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset(); - if (lastTapPosition >= 0) { - // Safety check, should not be possible. - if (lastTapPosition > mText.length()) { - Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs " - + mText.length() + ")"); - lastTapPosition = mText.length(); - } - return lastTapPosition; - } - } - - return -1; - } - - void onWindowFocusChanged(boolean hasWindowFocus) { - if (hasWindowFocus) { - if (mBlink != null) { - mBlink.uncancel(); - makeBlink(); - } - } else { - if (mBlink != null) { - mBlink.cancel(); - } - if (mInputContentType != null) { - mInputContentType.enterDown = false; - } - // Order matters! Must be done before onParentLostFocus to rely on isShowingUp - hideControllers(); - if (mSuggestionsPopupWindow != null) { - mSuggestionsPopupWindow.onParentLostFocus(); - } - - // Don't leave us in the middle of a batch edit. - onEndBatchEdit(); - } - } - - void onTouchEvent(MotionEvent event) { - if (hasSelectionController()) { - getSelectionController().onTouchEvent(event); - } - - if (mShowSuggestionRunnable != null) { - removeCallbacks(mShowSuggestionRunnable); - mShowSuggestionRunnable = null; - } - - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mLastDownPositionX = event.getX(); - mLastDownPositionY = event.getY(); - - // Reset this state; it will be re-set if super.onTouchEvent - // causes focus to move to the view. - mTouchFocusSelected = false; - mIgnoreActionUpEvent = false; - } - } - - void onDraw(Canvas canvas, Layout layout, Path highlight, int cursorOffsetVertical) { - final int selectionStart = getSelectionStart(); - final int selectionEnd = getSelectionEnd(); - - final InputMethodState ims = mInputMethodState; - if (ims != null && ims.mBatchEditNesting == 0) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - if (imm.isActive(TextView.this)) { - boolean reported = false; - if (ims.mContentChanged || ims.mSelectionModeChanged) { - // We are in extract mode and the content has changed - // in some way... just report complete new text to the - // input method. - reported = reportExtractedText(); - } - if (!reported && highlight != null) { - int candStart = -1; - int candEnd = -1; - if (mText instanceof Spannable) { - Spannable sp = (Spannable)mText; - candStart = EditableInputConnection.getComposingSpanStart(sp); - candEnd = EditableInputConnection.getComposingSpanEnd(sp); - } - imm.updateSelection(TextView.this, - selectionStart, selectionEnd, candStart, candEnd); - } - } - - if (imm.isWatchingCursor(TextView.this) && highlight != null) { - highlight.computeBounds(ims.mTmpRectF, true); - ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0; - - canvas.getMatrix().mapPoints(ims.mTmpOffset); - ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]); - - ims.mTmpRectF.offset(0, cursorOffsetVertical); - - ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5), - (int)(ims.mTmpRectF.top + 0.5), - (int)(ims.mTmpRectF.right + 0.5), - (int)(ims.mTmpRectF.bottom + 0.5)); - - imm.updateCursor(TextView.this, - ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top, - ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom); - } - } - } - - if (mCorrectionHighlighter != null) { - mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); - } - - if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) { - drawCursor(canvas, cursorOffsetVertical); - // Rely on the drawable entirely, do not draw the cursor line. - // Has to be done after the IMM related code above which relies on the highlight. - highlight = null; - } - - if (canHaveDisplayList() && canvas.isHardwareAccelerated()) { - drawHardwareAccelerated(canvas, layout, highlight, cursorOffsetVertical); - } else { - layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); - } - - if (mMarquee != null && mMarquee.shouldDrawGhost()) { - canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); - layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); - } - } - - private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, - int cursorOffsetVertical) { - final int width = mRight - mLeft; - final int height = mBottom - mTop; - - final long lineRange = layout.getLineRangeForDraw(canvas); - int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); - int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); - if (lastLine < 0) return; - - layout.drawBackground(canvas, highlight, mHighlightPaint, cursorOffsetVertical, - firstLine, lastLine); - - if (layout instanceof DynamicLayout) { - if (mTextDisplayLists == null) { - mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)]; - } - - DynamicLayout dynamicLayout = (DynamicLayout) layout; - int[] blockEnds = dynamicLayout.getBlockEnds(); - int[] blockIndices = dynamicLayout.getBlockIndices(); - final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); - - canvas.translate(mScrollX, mScrollY); - int endOfPreviousBlock = -1; - int searchStartIndex = 0; - for (int i = 0; i < numberOfBlocks; i++) { - int blockEnd = blockEnds[i]; - int blockIndex = blockIndices[i]; - - final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX; - if (blockIsInvalid) { - blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks, - searchStartIndex); - // Dynamic layout internal block indices structure is updated from Editor - blockIndices[i] = blockIndex; - searchStartIndex = blockIndex + 1; - } - - DisplayList blockDisplayList = mTextDisplayLists[blockIndex]; - if (blockDisplayList == null) { - blockDisplayList = mTextDisplayLists[blockIndex] = - getHardwareRenderer().createDisplayList("Text " + blockIndex); - } else { - if (blockIsInvalid) blockDisplayList.invalidate(); - } - - if (!blockDisplayList.isValid()) { - final HardwareCanvas hardwareCanvas = blockDisplayList.start(); - try { - hardwareCanvas.setViewport(width, height); - // The dirty rect should always be null for a display list - hardwareCanvas.onPreDraw(null); - hardwareCanvas.translate(-mScrollX, -mScrollY); - layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd); - hardwareCanvas.translate(mScrollX, mScrollY); - } finally { - hardwareCanvas.onPostDraw(); - blockDisplayList.end(); - if (USE_DISPLAY_LIST_PROPERTIES) { - blockDisplayList.setLeftTopRightBottom(0, 0, width, height); - } - } - } - - ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null, - DisplayList.FLAG_CLIP_CHILDREN); - endOfPreviousBlock = blockEnd; - } - canvas.translate(-mScrollX, -mScrollY); - } else { - // Fallback on the layout method (a BoringLayout is used when the text is empty) - layout.drawText(canvas, firstLine, lastLine); - } - } - - private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks, - int searchStartIndex) { - int length = mTextDisplayLists.length; - for (int i = searchStartIndex; i < length; i++) { - boolean blockIndexFound = false; - for (int j = 0; j < numberOfBlocks; j++) { - if (blockIndices[j] == i) { - blockIndexFound = true; - break; - } - } - if (blockIndexFound) continue; - return i; - } - - // No available index found, the pool has to grow - int newSize = ArrayUtils.idealIntArraySize(length + 1); - DisplayList[] displayLists = new DisplayList[newSize]; - System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length); - mTextDisplayLists = displayLists; - return length; - } - - private void drawCursor(Canvas canvas, int cursorOffsetVertical) { - final boolean translate = cursorOffsetVertical != 0; - if (translate) canvas.translate(0, cursorOffsetVertical); - for (int i = 0; i < getEditor().mCursorCount; i++) { - mCursorDrawable[i].draw(canvas); - } - if (translate) canvas.translate(0, -cursorOffsetVertical); - } - - private void invalidateTextDisplayList() { - if (mTextDisplayLists != null) { - for (int i = 0; i < mTextDisplayLists.length; i++) { - if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate(); - } - } - } - - private void updateCursorsPositions() { - if (mCursorDrawableRes == 0) { - mCursorCount = 0; - return; - } - - final int offset = getSelectionStart(); - final int line = mLayout.getLineForOffset(offset); - final int top = mLayout.getLineTop(line); - final int bottom = mLayout.getLineTop(line + 1); - - mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1; - - int middle = bottom; - if (mCursorCount == 2) { - // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)} - middle = (top + bottom) >> 1; - } - - updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset)); - - if (mCursorCount == 2) { - updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset)); - } - } - - private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { - if (mCursorDrawable[cursorIndex] == null) - mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes); - - if (mTempRect == null) mTempRect = new Rect(); - mCursorDrawable[cursorIndex].getPadding(mTempRect); - final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth(); - horizontal = Math.max(0.5f, horizontal - 0.5f); - final int left = (int) (horizontal) - mTempRect.left; - mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width, - bottom + mTempRect.bottom); - } } } diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java index f3486bd..3115eff 100644 --- a/core/java/com/android/internal/app/ActionBarImpl.java +++ b/core/java/com/android/internal/app/ActionBarImpl.java @@ -21,6 +21,7 @@ import com.android.internal.view.menu.MenuPopupHelper; import com.android.internal.view.menu.SubMenuBuilder; import com.android.internal.widget.ActionBarContainer; import com.android.internal.widget.ActionBarContextView; +import com.android.internal.widget.ActionBarOverlayLayout; import com.android.internal.widget.ActionBarView; import com.android.internal.widget.ScrollingTabContainerView; @@ -47,6 +48,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.view.Window; import android.view.accessibility.AccessibilityEvent; import android.widget.SpinnerAdapter; @@ -69,7 +71,9 @@ public class ActionBarImpl extends ActionBar { private Activity mActivity; private Dialog mDialog; + private ActionBarOverlayLayout mOverlayLayout; private ActionBarContainer mContainerView; + private ViewGroup mTopVisibilityView; private ActionBarView mActionView; private ActionBarContextView mContextView; private ActionBarContainer mSplitView; @@ -100,6 +104,8 @@ public class ActionBarImpl extends ActionBar { final Handler mHandler = new Handler(); Runnable mTabSelector; + private int mCurWindowVisibility = View.VISIBLE; + private Animator mCurrentShowAnim; private Animator mCurrentModeAnim; private boolean mShowHideAnimationEnabled; @@ -110,12 +116,12 @@ public class ActionBarImpl extends ActionBar { public void onAnimationEnd(Animator animation) { if (mContentView != null) { mContentView.setTranslationY(0); - mContainerView.setTranslationY(0); + mTopVisibilityView.setTranslationY(0); } if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) { mSplitView.setVisibility(View.GONE); } - mContainerView.setVisibility(View.GONE); + mTopVisibilityView.setVisibility(View.GONE); mContainerView.setTransitioning(false); mCurrentShowAnim = null; completeDeferredDestroyActionMode(); @@ -126,7 +132,7 @@ public class ActionBarImpl extends ActionBar { @Override public void onAnimationEnd(Animator animation) { mCurrentShowAnim = null; - mContainerView.requestLayout(); + mTopVisibilityView.requestLayout(); } }; @@ -147,11 +153,21 @@ public class ActionBarImpl extends ActionBar { private void init(View decor) { mContext = decor.getContext(); + mOverlayLayout = (ActionBarOverlayLayout) decor.findViewById( + com.android.internal.R.id.action_bar_overlay_layout); + if (mOverlayLayout != null) { + mOverlayLayout.setActionBar(this); + } mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar); mContextView = (ActionBarContextView) decor.findViewById( com.android.internal.R.id.action_context_bar); mContainerView = (ActionBarContainer) decor.findViewById( com.android.internal.R.id.action_bar_container); + mTopVisibilityView = (ViewGroup)decor.findViewById( + com.android.internal.R.id.top_action_bar); + if (mTopVisibilityView == null) { + mTopVisibilityView = mContainerView; + } mSplitView = (ActionBarContainer) decor.findViewById( com.android.internal.R.id.split_action_bar); @@ -190,11 +206,22 @@ public class ActionBarImpl extends ActionBar { } final boolean isInTabMode = getNavigationMode() == NAVIGATION_MODE_TABS; if (mTabScrollView != null) { - mTabScrollView.setVisibility(isInTabMode ? View.VISIBLE : View.GONE); + if (isInTabMode) { + mTabScrollView.setVisibility(View.VISIBLE); + if (mOverlayLayout != null) { + mOverlayLayout.requestFitSystemWindows(); + } + } else { + mTabScrollView.setVisibility(View.GONE); + } } mActionView.setCollapsable(!mHasEmbeddedTabs && isInTabMode); } + public boolean hasNonEmbeddedTabs() { + return !mHasEmbeddedTabs && getNavigationMode() == NAVIGATION_MODE_TABS; + } + private void ensureTabsExist() { if (mTabScrollView != null) { return; @@ -206,8 +233,14 @@ public class ActionBarImpl extends ActionBar { tabScroller.setVisibility(View.VISIBLE); mActionView.setEmbeddedTabView(tabScroller); } else { - tabScroller.setVisibility(getNavigationMode() == NAVIGATION_MODE_TABS ? - View.VISIBLE : View.GONE); + if (getNavigationMode() == NAVIGATION_MODE_TABS) { + tabScroller.setVisibility(View.VISIBLE); + if (mOverlayLayout != null) { + mOverlayLayout.requestFitSystemWindows(); + } + } else { + tabScroller.setVisibility(View.GONE); + } mContainerView.setTabContainer(tabScroller); } mTabScrollView = tabScroller; @@ -221,6 +254,10 @@ public class ActionBarImpl extends ActionBar { } } + public void setWindowVisibility(int visibility) { + mCurWindowVisibility = visibility; + } + /** * Enables or disables animation between show/hide states. * If animation is disabled using this method, animations in progress @@ -396,7 +433,12 @@ public class ActionBarImpl extends ActionBar { animateToMode(true); if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) { // TODO animate this - mSplitView.setVisibility(View.VISIBLE); + if (mSplitView.getVisibility() != View.VISIBLE) { + mSplitView.setVisibility(View.VISIBLE); + if (mOverlayLayout != null) { + mOverlayLayout.requestFitSystemWindows(); + } + } } mContextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); mActionMode = mode; @@ -530,28 +572,29 @@ public class ActionBarImpl extends ActionBar { @Override public void show() { - show(true); + show(true, false); } - void show(boolean markHiddenBeforeMode) { + public void show(boolean markHiddenBeforeMode, boolean alwaysAnimate) { if (mCurrentShowAnim != null) { mCurrentShowAnim.end(); } - if (mContainerView.getVisibility() == View.VISIBLE) { + if (mTopVisibilityView.getVisibility() == View.VISIBLE) { if (markHiddenBeforeMode) mWasHiddenBeforeMode = false; return; } - mContainerView.setVisibility(View.VISIBLE); + mTopVisibilityView.setVisibility(View.VISIBLE); - if (mShowHideAnimationEnabled) { - mContainerView.setAlpha(0); + if (mCurWindowVisibility == View.VISIBLE && (mShowHideAnimationEnabled + || alwaysAnimate)) { + mTopVisibilityView.setAlpha(0); AnimatorSet anim = new AnimatorSet(); - AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mContainerView, "alpha", 1)); + AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mTopVisibilityView, "alpha", 1)); if (mContentView != null) { b.with(ObjectAnimator.ofFloat(mContentView, "translationY", - -mContainerView.getHeight(), 0)); - mContainerView.setTranslationY(-mContainerView.getHeight()); - b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", 0)); + -mTopVisibilityView.getHeight(), 0)); + mTopVisibilityView.setTranslationY(-mTopVisibilityView.getHeight()); + b.with(ObjectAnimator.ofFloat(mTopVisibilityView, "translationY", 0)); } if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) { mSplitView.setAlpha(0); @@ -562,7 +605,7 @@ public class ActionBarImpl extends ActionBar { mCurrentShowAnim = anim; anim.start(); } else { - mContainerView.setAlpha(1); + mTopVisibilityView.setAlpha(1); mContainerView.setTranslationY(0); mShowListener.onAnimationEnd(null); } @@ -570,23 +613,28 @@ public class ActionBarImpl extends ActionBar { @Override public void hide() { + hide(false); + } + + public void hide(boolean alwaysAnimate) { if (mCurrentShowAnim != null) { mCurrentShowAnim.end(); } - if (mContainerView.getVisibility() == View.GONE) { + if (mTopVisibilityView.getVisibility() == View.GONE) { return; } - if (mShowHideAnimationEnabled) { - mContainerView.setAlpha(1); + if (mCurWindowVisibility == View.VISIBLE && (mShowHideAnimationEnabled + || alwaysAnimate)) { + mTopVisibilityView.setAlpha(1); mContainerView.setTransitioning(true); AnimatorSet anim = new AnimatorSet(); - AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mContainerView, "alpha", 0)); + AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mTopVisibilityView, "alpha", 0)); if (mContentView != null) { b.with(ObjectAnimator.ofFloat(mContentView, "translationY", - 0, -mContainerView.getHeight())); - b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", - -mContainerView.getHeight())); + 0, -mTopVisibilityView.getHeight())); + b.with(ObjectAnimator.ofFloat(mTopVisibilityView, "translationY", + -mTopVisibilityView.getHeight())); } if (mSplitView != null && mSplitView.getVisibility() == View.VISIBLE) { mSplitView.setAlpha(1); @@ -601,12 +649,12 @@ public class ActionBarImpl extends ActionBar { } public boolean isShowing() { - return mContainerView.getVisibility() == View.VISIBLE; + return mTopVisibilityView.getVisibility() == View.VISIBLE; } void animateToMode(boolean toActionMode) { if (toActionMode) { - show(false); + show(false, false); } if (mCurrentModeAnim != null) { mCurrentModeAnim.end(); @@ -980,6 +1028,11 @@ public class ActionBarImpl extends ActionBar { mTabScrollView.setVisibility(View.GONE); break; } + if (oldMode != mode && !mHasEmbeddedTabs) { + if (mOverlayLayout != null) { + mOverlayLayout.requestFitSystemWindows(); + } + } mActionView.setNavigationMode(mode); switch (mode) { case NAVIGATION_MODE_TABS: diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index aca1fa2..294d4c4 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -30,10 +30,12 @@ oneway interface IStatusBar void disable(int state); void animateExpand(); void animateCollapse(); - void setSystemUiVisibility(int vis); + void setSystemUiVisibility(int vis, int mask); void topAppWindowChanged(boolean menuVisible); void setImeWindowStatus(in IBinder token, int vis, int backDisposition); void setHardKeyboardStatus(boolean available, boolean enabled); void toggleRecentApps(); + void preloadRecentApps(); + void cancelPreloadRecentApps(); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index ecebfc0..c64f170 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -44,7 +44,9 @@ interface IStatusBarService int uid, int initialPid, String message); void onClearAllNotifications(); void onNotificationClear(String pkg, String tag, int id); - void setSystemUiVisibility(int vis); + void setSystemUiVisibility(int vis, int mask); void setHardKeyboardEnabled(boolean enabled); void toggleRecentApps(); + void preloadRecentApps(); + void cancelPreloadRecentApps(); } diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java new file mode 100644 index 0000000..8521481 --- /dev/null +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -0,0 +1,195 @@ +/* + * 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 com.android.internal.widget; + +import com.android.internal.app.ActionBarImpl; + +import android.animation.LayoutTransition; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; + +/** + * Special layout for the containing of an overlay action bar (and its + * content) to correctly handle fitting system windows when the content + * has request that its layout ignore them. + */ +public class ActionBarOverlayLayout extends FrameLayout { + private int mActionBarHeight; + private ActionBarImpl mActionBar; + private int mWindowVisibility = View.VISIBLE; + private View mContent; + private View mActionBarTop; + private ActionBarContainer mContainerView; + private ActionBarView mActionView; + private View mActionBarBottom; + private int mLastSystemUiVisibility; + private final Rect mZeroRect = new Rect(0, 0, 0, 0); + + static final int[] mActionBarSizeAttr = new int [] { + com.android.internal.R.attr.actionBarSize + }; + + public ActionBarOverlayLayout(Context context) { + super(context); + init(context); + } + + public ActionBarOverlayLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(Context context) { + TypedArray ta = getContext().getTheme().obtainStyledAttributes(mActionBarSizeAttr); + mActionBarHeight = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + } + + public void setActionBar(ActionBarImpl impl) { + mActionBar = impl; + if (getWindowToken() != null) { + // This is being initialized after being added to a window; + // make sure to update all state now. + mActionBar.setWindowVisibility(mWindowVisibility); + if (mLastSystemUiVisibility != 0) { + int newVis = mLastSystemUiVisibility; + onWindowSystemUiVisibilityChanged(newVis); + requestFitSystemWindows(); + } + } + } + + @Override + public void onWindowSystemUiVisibilityChanged(int visible) { + super.onWindowSystemUiVisibilityChanged(visible); + pullChildren(); + final int diff = mLastSystemUiVisibility ^ visible; + mLastSystemUiVisibility = visible; + final boolean barVisible = (visible&SYSTEM_UI_FLAG_FULLSCREEN) == 0; + final boolean wasVisible = mActionBar != null ? mActionBar.isShowing() : true; + if (barVisible != wasVisible || (diff&SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { + if (mActionBar != null) { + if (barVisible) mActionBar.show(true, true); + else mActionBar.hide(true); + requestFitSystemWindows(); + } + } + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + mWindowVisibility = visibility; + if (mActionBar != null) { + mActionBar.setWindowVisibility(visibility); + } + } + + private boolean applyInsets(View view, Rect insets, boolean left, boolean top, + boolean bottom, boolean right) { + boolean changed = false; + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)view.getLayoutParams(); + if (left && lp.leftMargin != insets.left) { + changed = true; + lp.leftMargin = insets.left; + } + if (top && lp.topMargin != insets.top) { + changed = true; + lp.topMargin = insets.top; + } + if (right && lp.rightMargin != insets.right) { + changed = true; + lp.rightMargin = insets.right; + } + if (bottom && lp.bottomMargin != insets.bottom) { + changed = true; + lp.bottomMargin = insets.bottom; + } + return changed; + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + pullChildren(); + + final int vis = getWindowSystemUiVisibility(); + final boolean stable = (vis & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0; + + // The top and bottom action bars are always within the content area. + boolean changed = applyInsets(mActionBarTop, insets, true, true, false, true); + if (mActionBarBottom != null) { + changed |= applyInsets(mActionBarBottom, insets, true, false, true, true); + } + + // If the window has not requested system UI layout flags, we need to + // make sure its content is not being covered by system UI... though it + // will still be covered by the action bar since they have requested it to + // overlay. + if ((vis & SYSTEM_UI_LAYOUT_FLAGS) == 0) { + changed |= applyInsets(mContent, insets, true, true, true, true); + // The insets are now consumed. + insets.set(0, 0, 0, 0); + } else { + changed |= applyInsets(mContent, mZeroRect, true, true, true, true); + } + + + if (stable || mActionBarTop.getVisibility() == VISIBLE) { + // The action bar creates additional insets for its content to use. + insets.top += mActionBarHeight; + } + + if (mActionBar != null && mActionBar.hasNonEmbeddedTabs()) { + View tabs = mContainerView.getTabContainer(); + if (stable || (tabs != null && tabs.getVisibility() == VISIBLE)) { + // If tabs are not embedded, adjust insets to account for them. + insets.top += mActionBarHeight; + } + } + + if (mActionView.isSplitActionBar()) { + if (stable || (mActionBarBottom != null + && mActionBarBottom.getVisibility() == VISIBLE)) { + // If action bar is split, adjust buttom insets for it. + insets.bottom += mActionBarHeight; + } + } + + if (changed) { + requestLayout(); + } + + return super.fitSystemWindows(insets); + } + + void pullChildren() { + if (mContent == null) { + mContent = findViewById(com.android.internal.R.id.content); + mActionBarTop = findViewById(com.android.internal.R.id.top_action_bar); + mContainerView = (ActionBarContainer)findViewById( + com.android.internal.R.id.action_bar_container); + mActionView = (ActionBarView) findViewById(com.android.internal.R.id.action_bar); + mActionBarBottom = findViewById(com.android.internal.R.id.split_action_bar); + } + } +} 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 @@ public class LockPatternUtils { * Calls back SetupFaceLock to delete the temporary gallery file */ public void deleteTempGallery() { - Intent intent = new Intent().setClassName("com.android.facelock", - "com.android.facelock.SetupFaceLock"); + Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY"); intent.putExtra("deleteTempGallery", true); - mContext.startActivity(intent); + mContext.sendBroadcast(intent); } /** @@ -449,10 +448,9 @@ public class LockPatternUtils { */ void deleteGallery() { if(usingBiometricWeak()) { - Intent intent = new Intent().setClassName("com.android.facelock", - "com.android.facelock.SetupFaceLock"); + Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY"); intent.putExtra("deleteGallery", true); - mContext.startActivity(intent); + mContext.sendBroadcast(intent); } } diff --git a/core/java/com/google/android/mms/pdu/PduPersister.java b/core/java/com/google/android/mms/pdu/PduPersister.java index 7c937ed..1a8e80f 100644 --- a/core/java/com/google/android/mms/pdu/PduPersister.java +++ b/core/java/com/google/android/mms/pdu/PduPersister.java @@ -519,98 +519,99 @@ public class PduPersister { * @throws MmsException Failed to load some fields of a PDU. */ public GenericPdu load(Uri uri) throws MmsException { - PduCacheEntry cacheEntry; - synchronized(PDU_CACHE_INSTANCE) { - if (PDU_CACHE_INSTANCE.isUpdating(uri)) { - if (LOCAL_LOGV) { - Log.v(TAG, "load: " + uri + " blocked by isUpdating()"); - } - try { - PDU_CACHE_INSTANCE.wait(); - } catch (InterruptedException e) { - Log.e(TAG, "load: ", e); - } - cacheEntry = PDU_CACHE_INSTANCE.get(uri); - if (cacheEntry != null) { - return cacheEntry.getPdu(); + GenericPdu pdu = null; + PduCacheEntry cacheEntry = null; + int msgBox = 0; + long threadId = -1; + try { + synchronized(PDU_CACHE_INSTANCE) { + if (PDU_CACHE_INSTANCE.isUpdating(uri)) { + if (LOCAL_LOGV) { + Log.v(TAG, "load: " + uri + " blocked by isUpdating()"); + } + try { + PDU_CACHE_INSTANCE.wait(); + } catch (InterruptedException e) { + Log.e(TAG, "load: ", e); + } + cacheEntry = PDU_CACHE_INSTANCE.get(uri); + if (cacheEntry != null) { + return cacheEntry.getPdu(); + } } + // Tell the cache to indicate to other callers that this item + // is currently being updated. + PDU_CACHE_INSTANCE.setUpdating(uri, true); } - // Tell the cache to indicate to other callers that this item - // is currently being updated. - PDU_CACHE_INSTANCE.setUpdating(uri, true); - } - Cursor c = SqliteWrapper.query(mContext, mContentResolver, uri, - PDU_PROJECTION, null, null, null); - PduHeaders headers = new PduHeaders(); - Set<Entry<Integer, Integer>> set; - long msgId = ContentUris.parseId(uri); - int msgBox; - long threadId; + Cursor c = SqliteWrapper.query(mContext, mContentResolver, uri, + PDU_PROJECTION, null, null, null); + PduHeaders headers = new PduHeaders(); + Set<Entry<Integer, Integer>> set; + long msgId = ContentUris.parseId(uri); - try { - if ((c == null) || (c.getCount() != 1) || !c.moveToFirst()) { - throw new MmsException("Bad uri: " + uri); - } + try { + if ((c == null) || (c.getCount() != 1) || !c.moveToFirst()) { + throw new MmsException("Bad uri: " + uri); + } - msgBox = c.getInt(PDU_COLUMN_MESSAGE_BOX); - threadId = c.getLong(PDU_COLUMN_THREAD_ID); + msgBox = c.getInt(PDU_COLUMN_MESSAGE_BOX); + threadId = c.getLong(PDU_COLUMN_THREAD_ID); - set = ENCODED_STRING_COLUMN_INDEX_MAP.entrySet(); - for (Entry<Integer, Integer> e : set) { - setEncodedStringValueToHeaders( - c, e.getValue(), headers, e.getKey()); - } + set = ENCODED_STRING_COLUMN_INDEX_MAP.entrySet(); + for (Entry<Integer, Integer> e : set) { + setEncodedStringValueToHeaders( + c, e.getValue(), headers, e.getKey()); + } - set = TEXT_STRING_COLUMN_INDEX_MAP.entrySet(); - for (Entry<Integer, Integer> e : set) { - setTextStringToHeaders( - c, e.getValue(), headers, e.getKey()); - } + set = TEXT_STRING_COLUMN_INDEX_MAP.entrySet(); + for (Entry<Integer, Integer> e : set) { + setTextStringToHeaders( + c, e.getValue(), headers, e.getKey()); + } - set = OCTET_COLUMN_INDEX_MAP.entrySet(); - for (Entry<Integer, Integer> e : set) { - setOctetToHeaders( - c, e.getValue(), headers, e.getKey()); - } + set = OCTET_COLUMN_INDEX_MAP.entrySet(); + for (Entry<Integer, Integer> e : set) { + setOctetToHeaders( + c, e.getValue(), headers, e.getKey()); + } - set = LONG_COLUMN_INDEX_MAP.entrySet(); - for (Entry<Integer, Integer> e : set) { - setLongToHeaders( - c, e.getValue(), headers, e.getKey()); - } - } finally { - if (c != null) { - c.close(); + set = LONG_COLUMN_INDEX_MAP.entrySet(); + for (Entry<Integer, Integer> e : set) { + setLongToHeaders( + c, e.getValue(), headers, e.getKey()); + } + } finally { + if (c != null) { + c.close(); + } } - } - - // Check whether 'msgId' has been assigned a valid value. - if (msgId == -1L) { - throw new MmsException("Error! ID of the message: -1."); - } - // Load address information of the MM. - loadAddress(msgId, headers); - - int msgType = headers.getOctet(PduHeaders.MESSAGE_TYPE); - PduBody body = new PduBody(); + // Check whether 'msgId' has been assigned a valid value. + if (msgId == -1L) { + throw new MmsException("Error! ID of the message: -1."); + } - // For PDU which type is M_retrieve.conf or Send.req, we should - // load multiparts and put them into the body of the PDU. - if ((msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) - || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) { - PduPart[] parts = loadParts(msgId); - if (parts != null) { - int partsNum = parts.length; - for (int i = 0; i < partsNum; i++) { - body.addPart(parts[i]); + // Load address information of the MM. + loadAddress(msgId, headers); + + int msgType = headers.getOctet(PduHeaders.MESSAGE_TYPE); + PduBody body = new PduBody(); + + // For PDU which type is M_retrieve.conf or Send.req, we should + // load multiparts and put them into the body of the PDU. + if ((msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) + || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) { + PduPart[] parts = loadParts(msgId); + if (parts != null) { + int partsNum = parts.length; + for (int i = 0; i < partsNum; i++) { + body.addPart(parts[i]); + } } } - } - GenericPdu pdu = null; - switch (msgType) { + switch (msgType) { case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: pdu = new NotificationInd(headers); break; @@ -657,16 +658,20 @@ public class PduPersister { default: throw new MmsException( "Unrecognized PDU type: " + Integer.toHexString(msgType)); + } + } finally { + synchronized(PDU_CACHE_INSTANCE) { + if (pdu != null) { + assert(PDU_CACHE_INSTANCE.get(uri) == null); + // Update the cache entry with the real info + cacheEntry = new PduCacheEntry(pdu, msgBox, threadId); + PDU_CACHE_INSTANCE.put(uri, cacheEntry); + } + PDU_CACHE_INSTANCE.setUpdating(uri, false); + PDU_CACHE_INSTANCE.notifyAll(); // tell anybody waiting on this entry to go ahead + } } - - synchronized(PDU_CACHE_INSTANCE ) { - assert(PDU_CACHE_INSTANCE.get(uri) == null); - // Update the cache entry with the real info - cacheEntry = new PduCacheEntry(pdu, msgBox, threadId); - PDU_CACHE_INSTANCE.put(uri, cacheEntry); - PDU_CACHE_INSTANCE.notifyAll(); // tell anybody waiting on this entry to go ahead - return pdu; - } + return pdu; } private void persistAddress( |
