summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java')
-rwxr-xr-xcore/java/android/animation/ValueAnimator.java2
-rw-r--r--core/java/android/app/ActionBar.java10
-rw-r--r--core/java/android/app/ActivityManagerNative.java25
-rw-r--r--core/java/android/app/IActivityManager.java7
-rw-r--r--core/java/android/app/Notification.java217
-rw-r--r--core/java/android/nfc/INfcAdapter.aidl2
-rw-r--r--core/java/android/nfc/INfcTag.aidl3
-rw-r--r--core/java/android/nfc/NfcActivityManager.java347
-rw-r--r--core/java/android/nfc/NfcAdapter.java241
-rw-r--r--core/java/android/nfc/NfcFragment.java96
-rw-r--r--core/java/android/nfc/tech/Ndef.java26
-rw-r--r--core/java/android/nfc/tech/NdefFormatable.java10
-rw-r--r--core/java/android/service/textservice/SpellCheckerService.java20
-rw-r--r--core/java/android/text/DynamicLayout.java18
-rw-r--r--core/java/android/text/SpannableStringBuilder.java8
-rw-r--r--core/java/android/view/Choreographer.java380
-rw-r--r--core/java/android/view/FocusFinder.java30
-rw-r--r--core/java/android/view/GLES20Canvas.java43
-rw-r--r--core/java/android/view/HardwareRenderer.java76
-rw-r--r--core/java/android/view/View.java265
-rw-r--r--core/java/android/view/ViewGroup.java50
-rw-r--r--core/java/android/view/ViewParent.java6
-rw-r--r--core/java/android/view/ViewRootImpl.java451
-rw-r--r--core/java/android/view/Window.java10
-rw-r--r--core/java/android/view/textservice/SentenceSuggestionsInfo.java20
-rw-r--r--core/java/android/view/textservice/SpellCheckerSession.java23
-rwxr-xr-xcore/java/android/webkit/GeolocationPermissions.java9
-rw-r--r--core/java/android/webkit/WebSettingsClassic.java11
-rw-r--r--core/java/android/webkit/WebStorage.java8
-rw-r--r--core/java/android/webkit/WebView.java2
-rw-r--r--core/java/android/webkit/WebViewClassic.java292
-rw-r--r--core/java/android/webkit/WebViewCore.java8
-rw-r--r--core/java/android/widget/Editor.java3750
-rw-r--r--core/java/android/widget/TextView.java3983
-rw-r--r--core/java/com/android/internal/app/ActionBarImpl.java107
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl4
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBarService.aidl4
-rw-r--r--core/java/com/android/internal/widget/ActionBarOverlayLayout.java195
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java10
-rw-r--r--core/java/com/android/internal/widget/PointerLocationView.java486
40 files changed, 6196 insertions, 5059 deletions
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index e2b8ce4..fade20c 100755
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -647,7 +647,7 @@ public class ValueAnimator extends Animator {
// onAnimate to process the next frame of the animations.
if (!mAnimationScheduled
&& (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty())) {
- mChoreographer.postAnimationCallback(this, null);
+ mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
mAnimationScheduled = true;
}
}
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/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index c402329..5917cbf 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -25,6 +25,7 @@ import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.ConfigurationInfo;
import android.content.pm.IPackageDataObserver;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.net.Uri;
@@ -1494,7 +1495,15 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM
reply.writeInt(result ? 1 : 0);
return true;
}
-
+
+ case GET_CURRENT_USER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ UserInfo userInfo = getCurrentUser();
+ reply.writeNoException();
+ userInfo.writeToParcel(reply, 0);
+ return true;
+ }
+
case REMOVE_SUB_TASK_TRANSACTION:
{
data.enforceInterface(IActivityManager.descriptor);
@@ -3530,7 +3539,19 @@ class ActivityManagerProxy implements IActivityManager
data.recycle();
return result;
}
-
+
+ public UserInfo getCurrentUser() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(SWITCH_USER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ UserInfo userInfo = UserInfo.CREATOR.createFromParcel(reply);
+ reply.recycle();
+ data.recycle();
+ return userInfo;
+ }
+
public boolean removeSubTask(int taskId, int subTaskIndex) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 1d994d8..2b1eb43 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -28,6 +28,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.ConfigurationInfo;
import android.content.pm.IPackageDataObserver;
import android.content.pm.ProviderInfo;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.net.Uri;
@@ -318,10 +319,11 @@ public interface IActivityManager extends IInterface {
public boolean getPackageAskScreenCompat(String packageName) throws RemoteException;
public void setPackageAskScreenCompat(String packageName, boolean ask)
throws RemoteException;
-
+
// Multi-user APIs
public boolean switchUser(int userid) throws RemoteException;
-
+ public UserInfo getCurrentUser() throws RemoteException;
+
public boolean removeSubTask(int taskId, int subTaskIndex) throws RemoteException;
public boolean removeTask(int taskId, int flags) throws RemoteException;
@@ -575,4 +577,5 @@ public interface IActivityManager extends IInterface {
int REMOVE_CONTENT_PROVIDER_EXTERNAL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+141;
int GET_MY_MEMORY_STATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+142;
int KILL_PROCESSES_BELOW_FOREGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+143;
+ int GET_CURRENT_USER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+144;
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index bbb6a4e..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;
@@ -187,7 +188,6 @@ public class Notification implements Parcelable
*/
public RemoteViews contentView;
-
/**
* The view that will represent this notification in the pop-up "intruder alert" dialog.
* @hide
@@ -195,6 +195,14 @@ public class Notification implements Parcelable
public RemoteViews intruderView;
/**
+ * A larger version of {@link #contentView}, giving the Notification an
+ * opportunity to show more detail. The system UI may choose to show this
+ * instead of the normal content view at its discretion.
+ * @hide
+ */
+ public RemoteViews bigContentView;
+
+ /**
* The bitmap that may escape the bounds of the panel and bar.
*/
public Bitmap largeIcon;
@@ -584,6 +592,9 @@ public class Notification implements Parcelable
if (parcel.readInt() != 0) {
intruderView = RemoteViews.CREATOR.createFromParcel(parcel);
}
+ if (parcel.readInt() != 0) {
+ bigContentView = RemoteViews.CREATOR.createFromParcel(parcel);
+ }
}
@Override
@@ -650,6 +661,9 @@ public class Notification implements Parcelable
if (this.intruderView != null) {
that.intruderView = this.intruderView.clone();
}
+ if (this.bigContentView != null) {
+ that.bigContentView = this.bigContentView.clone();
+ }
return that;
}
@@ -747,6 +761,13 @@ public class Notification implements Parcelable
} else {
parcel.writeInt(0);
}
+
+ if (bigContentView != null) {
+ parcel.writeInt(1);
+ bigContentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
}
/**
@@ -788,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);
}
@@ -896,6 +917,7 @@ public class Notification implements Parcelable
private CharSequence mContentTitle;
private CharSequence mContentText;
private CharSequence mContentInfo;
+ private CharSequence mSubText;
private PendingIntent mContentIntent;
private RemoteViews mContentView;
private PendingIntent mDeleteIntent;
@@ -1013,6 +1035,15 @@ public class Notification implements Parcelable
}
/**
+ * Set the third line of text in the platform notification template.
+ * Don't use if you're also using {@link #setProgress(int, int, boolean)}; they occupy the same location in the standard template.
+ */
+ public Builder setSubText(CharSequence text) {
+ mSubText = text;
+ return this;
+ }
+
+ /**
* Set the large number at the right-hand side of the notification. This is
* equivalent to setContentInfo, although it might show the number in a different
* font size for readability.
@@ -1025,7 +1056,6 @@ public class Notification implements Parcelable
/**
* A small piece of additional information pertaining to this notification.
*
-
* The platform template will draw this on the last line of the notification, at the far
* right (to the right of a smallIcon if it has been placed there).
*/
@@ -1037,7 +1067,6 @@ public class Notification implements Parcelable
/**
* Set the progress this notification represents.
*
-
* The platform template will represent this using a {@link ProgressBar}.
*/
public Builder setProgress(int max, int progress, boolean indeterminate) {
@@ -1050,7 +1079,6 @@ public class Notification implements Parcelable
/**
* Supply a custom RemoteViews to use instead of the platform template.
*
-
* @see Notification#contentView
*/
public Builder setContent(RemoteViews views) {
@@ -1061,17 +1089,12 @@ public class Notification implements Parcelable
/**
* Supply a {@link PendingIntent} to be sent when the notification is clicked.
*
-
* As of {@link android.os.Build.VERSION_CODES#HONEYCOMB}, if this field is unset and you
* have specified a custom RemoteViews with {@link #setContent(RemoteViews)}, you can use
* {@link RemoteViews#setOnClickPendingIntent RemoteViews.setOnClickPendingIntent(int,PendingIntent)}
-
* to assign PendingIntents to individual views in that custom layout (i.e., to create
-
- * clickable buttons inside the
- * notification view).
+ * clickable buttons inside the notification view).
*
-
* @see Notification#contentIntent Notification.contentIntent
*/
public Builder setContentIntent(PendingIntent intent) {
@@ -1082,7 +1105,6 @@ public class Notification implements Parcelable
/**
* Supply a {@link PendingIntent} to send when the notification is cleared explicitly by the user.
*
-
* @see Notification#deleteIntent
*/
public Builder setDeleteIntent(PendingIntent intent) {
@@ -1115,7 +1137,6 @@ public class Notification implements Parcelable
* Set the "ticker" text which is displayed in the status bar when the notification first
* arrives.
*
-
* @see Notification#tickerText
*/
public Builder setTicker(CharSequence tickerText) {
@@ -1355,20 +1376,28 @@ public class Notification implements Parcelable
}
}
- private RemoteViews makeRemoteViews(int resId) {
+ private RemoteViews applyStandardTemplate(int resId) {
RemoteViews contentView = new RemoteViews(mContext.getPackageName(), resId);
boolean hasLine3 = false;
+ boolean hasLine2 = false;
+ int smallIconImageViewId = R.id.icon;
+ if (mLargeIcon != null) {
+ contentView.setImageViewBitmap(R.id.icon, mLargeIcon);
+ smallIconImageViewId = R.id.right_icon;
+ }
if (mSmallIcon != 0) {
- contentView.setImageViewResource(R.id.icon, mSmallIcon);
- contentView.setViewVisibility(R.id.icon, View.VISIBLE);
+ contentView.setImageViewResource(smallIconImageViewId, mSmallIcon);
+ contentView.setViewVisibility(smallIconImageViewId, View.VISIBLE);
} else {
- contentView.setViewVisibility(R.id.icon, View.GONE);
+ contentView.setViewVisibility(smallIconImageViewId, View.GONE);
}
if (mContentTitle != null) {
contentView.setTextViewText(R.id.title, mContentTitle);
}
if (mContentText != null) {
- contentView.setTextViewText(R.id.text, mContentText);
+ contentView.setTextViewText(
+ (mSubText != null) ? R.id.text2 : R.id.text,
+ mContentText);
hasLine3 = true;
}
if (mContentInfo != null) {
@@ -1390,12 +1419,19 @@ public class Notification implements Parcelable
} else {
contentView.setViewVisibility(R.id.info, View.GONE);
}
- if (mProgressMax != 0 || mProgressIndeterminate) {
- contentView.setProgressBar(
- R.id.progress, mProgressMax, mProgress, mProgressIndeterminate);
- contentView.setViewVisibility(R.id.progress, View.VISIBLE);
+
+ if (mSubText != null) {
+ contentView.setTextViewText(R.id.text, mSubText);
+ contentView.setViewVisibility(R.id.text2, View.VISIBLE);
} else {
- contentView.setViewVisibility(R.id.progress, View.GONE);
+ contentView.setViewVisibility(R.id.text2, View.GONE);
+ if (mProgressMax != 0 || mProgressIndeterminate) {
+ contentView.setProgressBar(
+ R.id.progress, mProgressMax, mProgress, mProgressIndeterminate);
+ contentView.setViewVisibility(R.id.progress, View.VISIBLE);
+ } else {
+ contentView.setViewVisibility(R.id.progress, View.GONE);
+ }
}
if (mWhen != 0) {
contentView.setLong(R.id.time, "setTime", mWhen);
@@ -1404,13 +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 makeRemoteViews(mLargeIcon == null
- ? R.layout.status_bar_latest_event_content
- : R.layout.status_bar_latest_event_content_large_icon);
+ return applyStandardTemplate(R.layout.notification_template_base); // no more special large_icon flavor
}
}
@@ -1419,7 +1470,7 @@ public class Notification implements Parcelable
return mTickerView;
} else {
if (mContentView == null) {
- return makeRemoteViews(mLargeIcon == null
+ return applyStandardTemplate(mLargeIcon == null
? R.layout.status_bar_latest_event_ticker
: R.layout.status_bar_latest_event_ticker_large_icon);
} else {
@@ -1428,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);
@@ -1467,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.
@@ -1495,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;
}
@@ -1516,4 +1583,100 @@ public class Notification implements Parcelable
return n;
}
}
+
+ /**
+ * @hide because this API is still very rough
+ *
+ * This is a "rebuilder": It consumes a Builder object and modifies its output.
+ *
+ * This represents the "big picture" style notification, with a large Bitmap atop the usual notification.
+ *
+ * Usage:
+ * <pre class="prettyprint">
+ * Notification noti = new Notification.BigPictureStyle(
+ * new Notification.Builder()
+ * .setContentTitle(&quot;New mail from &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .setLargeIcon(aBitmap))
+ * .bigPicture(aBigBitmap)
+ * .build();
+ * </pre>
+ */
+ public static class BigPictureStyle {
+ private Builder mBuilder;
+ private Bitmap mPicture;
+
+ public BigPictureStyle(Builder builder) {
+ mBuilder = builder;
+ }
+
+ public BigPictureStyle bigPicture(Bitmap b) {
+ mPicture = b;
+ return this;
+ }
+
+ private RemoteViews makeBigContentView() {
+ RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(R.layout.notification_template_big_picture);
+
+ contentView.setImageViewBitmap(R.id.big_picture, mPicture);
+
+ return contentView;
+ }
+
+ public Notification build() {
+ Notification wip = mBuilder.getNotification();
+ wip.bigContentView = makeBigContentView();
+ return wip;
+ }
+ }
+
+ /**
+ * @hide because this API is still very rough
+ *
+ * This is a "rebuilder": It consumes a Builder object and modifies its output.
+ *
+ * This represents the "big text" style notification, with more area for the main content text to be read in its entirety.
+ *
+ * Usage:
+ * <pre class="prettyprint">
+ * Notification noti = new Notification.BigPictureStyle(
+ * new Notification.Builder()
+ * .setContentTitle(&quot;New mail from &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .setLargeIcon(aBitmap))
+ * .bigText(aVeryLongString)
+ * .build();
+ * </pre>
+ */
+ public static class BigTextStyle {
+ private Builder mBuilder;
+ private CharSequence mBigText;
+
+ public BigTextStyle(Builder builder) {
+ mBuilder = builder;
+ }
+
+ public BigTextStyle bigText(CharSequence cs) {
+ mBigText = cs;
+ return this;
+ }
+
+ private RemoteViews makeBigContentView() {
+ RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(R.layout.notification_template_base);
+
+ contentView.setTextViewText(R.id.big_text, mBigText);
+ contentView.setViewVisibility(R.id.big_text, View.VISIBLE);
+ contentView.setTextViewText(R.id.text, ""); // XXX: what do do with this spot?
+
+ return contentView;
+ }
+
+ public Notification build() {
+ Notification wip = mBuilder.getNotification();
+ wip.bigContentView = makeBigContentView();
+ return wip;
+ }
+ }
}
diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl
index 10da9ef..ddd00a4 100644
--- a/core/java/android/nfc/INfcAdapter.aidl
+++ b/core/java/android/nfc/INfcAdapter.aidl
@@ -42,7 +42,7 @@ interface INfcAdapter
void setForegroundDispatch(in PendingIntent intent,
in IntentFilter[] filters, in TechListParcel techLists);
- void setForegroundNdefPush(in NdefMessage msg, in INdefPushCallback callback);
+ void setNdefPushCallback(in INdefPushCallback callback);
void dispatch(in Tag tag);
diff --git a/core/java/android/nfc/INfcTag.aidl b/core/java/android/nfc/INfcTag.aidl
index bb5a9fd..2223255 100644
--- a/core/java/android/nfc/INfcTag.aidl
+++ b/core/java/android/nfc/INfcTag.aidl
@@ -29,13 +29,10 @@ 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);
- int getLastError(int nativeHandle);
-
NdefMessage ndefRead(int nativeHandle);
int ndefWrite(int nativeHandle, in NdefMessage msg);
int ndefMakeReadOnly(int nativeHandle);
diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java
index 5fe58e9..2c73056 100644
--- a/core/java/android/nfc/NfcActivityManager.java
+++ b/core/java/android/nfc/NfcActivityManager.java
@@ -17,210 +17,303 @@
package android.nfc;
import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
-import java.util.WeakHashMap;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
/**
* Manages NFC API's that are coupled to the life-cycle of an Activity.
*
- * <p>Uses a fragment to hook into onPause() and onResume() of the host
- * activities.
- *
- * <p>Ideally all of this management would be done in the NFC Service,
- * but right now it is much easier to do it in the application process.
+ * <p>Uses {@link Application#registerActivityLifecycleCallbacks} to hook
+ * into activity life-cycle events such as onPause() and onResume().
*
* @hide
*/
-public final class NfcActivityManager extends INdefPushCallback.Stub {
+public final class NfcActivityManager extends INdefPushCallback.Stub
+ implements Application.ActivityLifecycleCallbacks {
static final String TAG = NfcAdapter.TAG;
static final Boolean DBG = false;
final NfcAdapter mAdapter;
- final WeakHashMap<Activity, NfcActivityState> mNfcState; // contents protected by this
- final NfcEvent mDefaultEvent; // can re-use one NfcEvent because it just contains adapter
+ final NfcEvent mDefaultEvent; // cached NfcEvent (its currently always the same)
+
+ // All objects in the lists are protected by this
+ final List<NfcApplicationState> mApps; // Application(s) that have NFC state. Usually one
+ final List<NfcActivityState> mActivities; // Activities that have NFC state
/**
- * NFC state associated with an {@link Activity}
+ * NFC State associated with an {@link Application}.
*/
- class NfcActivityState {
- boolean resumed = false; // is the activity resumed
- NdefMessage ndefMessage;
- NfcAdapter.CreateNdefMessageCallback ndefMessageCallback;
- NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback;
- @Override
- public String toString() {
- StringBuilder s = new StringBuilder("[").append(resumed).append(" ");
- s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" ");
- s.append(onNdefPushCompleteCallback).append("]");
- return s.toString();
+ class NfcApplicationState {
+ int refCount = 0;
+ final Application app;
+ public NfcApplicationState(Application app) {
+ this.app = app;
+ }
+ public void register() {
+ refCount++;
+ if (refCount == 1) {
+ this.app.registerActivityLifecycleCallbacks(NfcActivityManager.this);
+ }
+ }
+ public void unregister() {
+ refCount--;
+ if (refCount == 0) {
+ this.app.unregisterActivityLifecycleCallbacks(NfcActivityManager.this);
+ } else if (refCount < 0) {
+ Log.e(TAG, "-ve refcount for " + app);
+ }
}
}
- public NfcActivityManager(NfcAdapter adapter) {
- mAdapter = adapter;
- mNfcState = new WeakHashMap<Activity, NfcActivityState>();
- mDefaultEvent = new NfcEvent(mAdapter);
+ NfcApplicationState findAppState(Application app) {
+ for (NfcApplicationState appState : mApps) {
+ if (appState.app == app) {
+ return appState;
+ }
+ }
+ return null;
}
- /**
- * onResume hook from fragment attached to activity
- */
- public synchronized void onResume(Activity activity) {
- NfcActivityState state = mNfcState.get(activity);
- if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state);
- if (state != null) {
- state.resumed = true;
- updateNfcService(state);
+ void registerApplication(Application app) {
+ NfcApplicationState appState = findAppState(app);
+ if (appState == null) {
+ appState = new NfcApplicationState(app);
+ mApps.add(appState);
}
+ appState.register();
}
- /**
- * onPause hook from fragment attached to activity
- */
- public synchronized void onPause(Activity activity) {
- NfcActivityState state = mNfcState.get(activity);
- if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state);
- if (state != null) {
- state.resumed = false;
- updateNfcService(state);
+ void unregisterApplication(Application app) {
+ NfcApplicationState appState = findAppState(app);
+ if (appState == null) {
+ Log.e(TAG, "app was not registered " + app);
+ return;
}
+ appState.unregister();
}
/**
- * onDestroy hook from fragment attached to activity
+ * NFC state associated with an {@link Activity}
*/
- public void onDestroy(Activity activity) {
- mNfcState.remove(activity);
- }
-
- public synchronized void setNdefPushMessage(Activity activity, NdefMessage message) {
- NfcActivityState state = getOrCreateState(activity, message != null);
- if (state == null || state.ndefMessage == message) {
- return; // nothing more to do;
+ class NfcActivityState {
+ boolean resumed = false;
+ Activity activity;
+ NdefMessage ndefMessage = null; // static NDEF message
+ NfcAdapter.CreateNdefMessageCallback ndefMessageCallback = null;
+ NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback = null;
+ public NfcActivityState(Activity activity) {
+ if (activity.getWindow().isDestroyed()) {
+ throw new IllegalStateException("activity is already destroyed");
+ }
+ this.activity = activity;
+ registerApplication(activity.getApplication());
}
- state.ndefMessage = message;
- if (message == null) {
- maybeRemoveState(activity, state);
+ public void destroy() {
+ unregisterApplication(activity.getApplication());
+ resumed = false;
+ activity = null;
+ ndefMessage = null;
+ ndefMessageCallback = null;
+ onNdefPushCompleteCallback = null;
}
- if (state.resumed) {
- updateNfcService(state);
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder("[").append(" ");
+ s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" ");
+ s.append(onNdefPushCompleteCallback).append("]");
+ return s.toString();
}
}
- public synchronized void setNdefPushMessageCallback(Activity activity,
- NfcAdapter.CreateNdefMessageCallback callback) {
- NfcActivityState state = getOrCreateState(activity, callback != null);
- if (state == null || state.ndefMessageCallback == callback) {
- return; // nothing more to do;
+ /** find activity state from mActivities */
+ synchronized NfcActivityState findActivityState(Activity activity) {
+ for (NfcActivityState state : mActivities) {
+ if (state.activity == activity) {
+ return state;
+ }
}
- state.ndefMessageCallback = callback;
- if (callback == null) {
- maybeRemoveState(activity, state);
+ return null;
+ }
+
+ /** find or create activity state from mActivities */
+ synchronized NfcActivityState getActivityState(Activity activity) {
+ NfcActivityState state = findActivityState(activity);
+ if (state == null) {
+ state = new NfcActivityState(activity);
+ mActivities.add(state);
}
- if (state.resumed) {
- updateNfcService(state);
+ return state;
+ }
+
+ synchronized NfcActivityState findResumedActivityState() {
+ for (NfcActivityState state : mActivities) {
+ if (state.resumed) {
+ return state;
+ }
}
+ return null;
}
- public synchronized void setOnNdefPushCompleteCallback(Activity activity,
- NfcAdapter.OnNdefPushCompleteCallback callback) {
- NfcActivityState state = getOrCreateState(activity, callback != null);
- if (state == null || state.onNdefPushCompleteCallback == callback) {
- return; // nothing more to do;
+ synchronized void destroyActivityState(Activity activity) {
+ NfcActivityState activityState = findActivityState(activity);
+ if (activityState != null) {
+ activityState.destroy();
+ mActivities.remove(activityState);
}
- state.onNdefPushCompleteCallback = callback;
- if (callback == null) {
- maybeRemoveState(activity, state);
+ }
+
+ public NfcActivityManager(NfcAdapter adapter) {
+ mAdapter = adapter;
+ mActivities = new LinkedList<NfcActivityState>();
+ mApps = new ArrayList<NfcApplicationState>(1); // Android VM usually has 1 app
+ mDefaultEvent = new NfcEvent(mAdapter);
+ }
+
+ public void setNdefPushMessage(Activity activity, NdefMessage message) {
+ boolean isResumed;
+ synchronized (NfcActivityManager.this) {
+ NfcActivityState state = getActivityState(activity);
+ state.ndefMessage = message;
+ isResumed = state.resumed;
}
- if (state.resumed) {
- updateNfcService(state);
+ if (isResumed) {
+ requestNfcServiceCallback(true);
}
}
- /**
- * Get the NfcActivityState for the specified Activity.
- * If create is true, then create it if it doesn't already exist,
- * and ensure the NFC fragment is attached to the activity.
- */
- synchronized NfcActivityState getOrCreateState(Activity activity, boolean create) {
- if (DBG) Log.d(TAG, "getOrCreateState " + activity + " " + create);
- NfcActivityState state = mNfcState.get(activity);
- if (state == null && create) {
- state = new NfcActivityState();
- mNfcState.put(activity, state);
- NfcFragment.attach(activity);
+ public void setNdefPushMessageCallback(Activity activity,
+ NfcAdapter.CreateNdefMessageCallback callback) {
+ boolean isResumed;
+ synchronized (NfcActivityManager.this) {
+ NfcActivityState state = getActivityState(activity);
+ state.ndefMessageCallback = callback;
+ isResumed = state.resumed;
+ }
+ if (isResumed) {
+ requestNfcServiceCallback(true);
}
- return state;
}
- /**
- * If the NfcActivityState is empty then remove it, and
- * detach it from the Activity.
- */
- synchronized void maybeRemoveState(Activity activity, NfcActivityState state) {
- if (state.ndefMessage == null && state.ndefMessageCallback == null &&
- state.onNdefPushCompleteCallback == null) {
- NfcFragment.remove(activity);
- mNfcState.remove(activity);
+ public void setOnNdefPushCompleteCallback(Activity activity,
+ NfcAdapter.OnNdefPushCompleteCallback callback) {
+ boolean isResumed;
+ synchronized (NfcActivityManager.this) {
+ NfcActivityState state = getActivityState(activity);
+ state.onNdefPushCompleteCallback = callback;
+ isResumed = state.resumed;
+ }
+ if (isResumed) {
+ requestNfcServiceCallback(true);
}
}
/**
- * Register NfcActivityState with the NFC service.
+ * Request or unrequest NFC service callbacks for NDEF push.
+ * Makes IPC call - do not hold lock.
+ * TODO: Do not do IPC on every onPause/onResume
*/
- synchronized void updateNfcService(NfcActivityState state) {
- boolean serviceCallbackNeeded = state.ndefMessageCallback != null ||
- state.onNdefPushCompleteCallback != null;
-
+ void requestNfcServiceCallback(boolean request) {
try {
- NfcAdapter.sService.setForegroundNdefPush(state.resumed ? state.ndefMessage : null,
- state.resumed && serviceCallbackNeeded ? this : null);
+ NfcAdapter.sService.setNdefPushCallback(request ? this : null);
} catch (RemoteException e) {
mAdapter.attemptDeadServiceRecovery(e);
}
}
- /**
- * Callback from NFC service
- */
+ /** Callback from NFC service, usually on binder thread */
@Override
public NdefMessage createMessage() {
- NfcAdapter.CreateNdefMessageCallback callback = null;
+ NfcAdapter.CreateNdefMessageCallback callback;
+ NdefMessage message;
synchronized (NfcActivityManager.this) {
- for (NfcActivityState state : mNfcState.values()) {
- if (state.resumed) {
- callback = state.ndefMessageCallback;
- }
- }
+ NfcActivityState state = findResumedActivityState();
+ if (state == null) return null;
+
+ callback = state.ndefMessageCallback;
+ message = state.ndefMessage;
}
- // drop lock before making callback
+ // Make callback without lock
if (callback != null) {
return callback.createNdefMessage(mDefaultEvent);
+ } else {
+ return message;
}
- return null;
}
- /**
- * Callback from NFC service
- */
+ /** Callback from NFC service, usually on binder thread */
@Override
public void onNdefPushComplete() {
- NfcAdapter.OnNdefPushCompleteCallback callback = null;
+ NfcAdapter.OnNdefPushCompleteCallback callback;
synchronized (NfcActivityManager.this) {
- for (NfcActivityState state : mNfcState.values()) {
- if (state.resumed) {
- callback = state.onNdefPushCompleteCallback;
- }
- }
+ NfcActivityState state = findResumedActivityState();
+ if (state == null) return;
+
+ callback = state.onNdefPushCompleteCallback;
}
- // drop lock before making callback
+ // Make callback without lock
if (callback != null) {
callback.onNdefPushComplete(mDefaultEvent);
}
}
+ /** Callback from Activity life-cycle, on main thread */
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) { /* NO-OP */ }
+
+ /** Callback from Activity life-cycle, on main thread */
+ @Override
+ public void onActivityStarted(Activity activity) { /* NO-OP */ }
+
+ /** Callback from Activity life-cycle, on main thread */
+ @Override
+ public void onActivityResumed(Activity activity) {
+ synchronized (NfcActivityManager.this) {
+ NfcActivityState state = findActivityState(activity);
+ if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state);
+ if (state == null) return;
+ state.resumed = true;
+ }
+ requestNfcServiceCallback(true);
+ }
+
+ /** Callback from Activity life-cycle, on main thread */
+ @Override
+ public void onActivityPaused(Activity activity) {
+ synchronized (NfcActivityManager.this) {
+ NfcActivityState state = findActivityState(activity);
+ if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state);
+ if (state == null) return;
+ state.resumed = false;
+ }
+ requestNfcServiceCallback(false);
+ }
+
+ /** Callback from Activity life-cycle, on main thread */
+ @Override
+ public void onActivityStopped(Activity activity) { /* NO-OP */ }
+
+ /** Callback from Activity life-cycle, on main thread */
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) { /* NO-OP */ }
+
+ /** Callback from Activity life-cycle, on main thread */
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ synchronized (NfcActivityManager.this) {
+ NfcActivityState state = findActivityState(activity);
+ if (DBG) Log.d(TAG, "onDestroy() for " + activity + " " + state);
+ if (state != null) {
+ // release all associated references
+ destroyActivityState(activity);
+ }
+ }
+ }
}
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index 23f96e3..b7a7bd5 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -556,109 +556,230 @@ public final class NfcAdapter {
}
/**
- * Set the {@link NdefMessage} to push over NFC during the specified activities.
+ * Set a static {@link NdefMessage} to send using Android Beam (TM).
*
- * <p>This method may be called at any time, but the NDEF message is
- * only made available for NDEF push when one of the specified activities
- * is in resumed (foreground) state.
+ * <p>This method may be called at any time before {@link Activity#onDestroy},
+ * but the NDEF message is only made available for NDEF push when the
+ * specified activity(s) are in resumed (foreground) state. The recommended
+ * approach is to call this method during your Activity's
+ * {@link Activity#onCreate} - see sample
+ * code below. This method does not immediately perform any I/O or blocking work,
+ * so is safe to call on your main thread.
*
* <p>Only one NDEF message can be pushed by the currently resumed activity.
* If both {@link #setNdefPushMessage} and
- * {@link #setNdefPushMessageCallback} are set then
+ * {@link #setNdefPushMessageCallback} are set, then
* the callback will take priority.
*
- * <p>Pass a null NDEF message to disable foreground NDEF push in the
- * specified activities.
+ * <p>If neither {@link #setNdefPushMessage} or
+ * {@link #setNdefPushMessageCallback} have been called for your activity, then
+ * the Android OS may choose to send a default NDEF message on your behalf,
+ * such as a URI for your application.
*
- * <p>At least one activity must be specified, and usually only one is necessary.
+ * <p>If {@link #setNdefPushMessage} is called with a null NDEF message,
+ * and/or {@link #setNdefPushMessageCallback} is called with a null callback,
+ * then NDEF push will be completely disabled for the specified activity(s).
+ * This also disables any default NDEF message the Android OS would have
+ * otherwise sent on your behalf.
+ *
+ * <p>The API allows for multiple activities to be specified at a time,
+ * but it is strongly recommended to just register one at a time,
+ * and to do so during the activity's {@link Activity#onCreate}. For example:
+ * <pre>
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ * if (nfcAdapter == null) return; // NFC not available on this device
+ * nfcAdapter.setNdefPushMessage(ndefMessage, this);
+ * }
+ * </pre>
+ * And that is it. Only one call per activity is necessary. The Android
+ * OS will automatically release its references to the NDEF message and the
+ * Activity object when it is destroyed if you follow this pattern.
+ *
+ * <p>If your Activity wants to dynamically generate an NDEF message,
+ * then set a callback using {@link #setNdefPushMessageCallback} instead
+ * of a static message.
+ *
+ * <p class="note">Do not pass in an Activity that has already been through
+ * {@link Activity#onDestroy}. This is guaranteed if you call this API
+ * during {@link Activity#onCreate}.
*
* <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
*
* @param message NDEF message to push over NFC, or null to disable
- * @param activity an activity in which NDEF push should be enabled to share the provided
- * NDEF message
- * @param activities optional additional activities that should also enable NDEF push with
- * the provided NDEF message
+ * @param activity activity for which the NDEF message will be pushed
+ * @param activities optional additional activities, however we strongly recommend
+ * to only register one at a time, and to do so in that activity's
+ * {@link Activity#onCreate}
*/
public void setNdefPushMessage(NdefMessage message, Activity activity,
Activity ... activities) {
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setNdefPushMessage(activity, message);
- for (Activity a : activities) {
- if (a == null) {
- throw new NullPointerException("activities cannot contain null");
+ int targetSdkVersion = getSdkVersion();
+ try {
+ if (activity == null) {
+ throw new NullPointerException("activity cannot be null");
+ }
+ mNfcActivityManager.setNdefPushMessage(activity, message);
+ for (Activity a : activities) {
+ if (a == null) {
+ throw new NullPointerException("activities cannot contain null");
+ }
+ mNfcActivityManager.setNdefPushMessage(a, message);
+ }
+ } catch (IllegalStateException e) {
+ if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ // Less strict on old applications - just log the error
+ Log.e(TAG, "Cannot call API with Activity that has already " +
+ "been destroyed", e);
+ } else {
+ // Prevent new applications from making this mistake, re-throw
+ throw(e);
}
- mNfcActivityManager.setNdefPushMessage(a, message);
}
}
/**
- * Set the callback to create a {@link NdefMessage} to push over NFC.
+ * Set a callback that dynamically generates NDEF messages to send using Android Beam (TM).
*
- * <p>This method may be called at any time, but this callback is
- * only made if one of the specified activities
- * is in resumed (foreground) state.
+ * <p>This method may be called at any time before {@link Activity#onDestroy},
+ * but the NDEF message callback can only occur when the
+ * specified activity(s) are in resumed (foreground) state. The recommended
+ * approach is to call this method during your Activity's
+ * {@link Activity#onCreate} - see sample
+ * code below. This method does not immediately perform any I/O or blocking work,
+ * so is safe to call on your main thread.
*
* <p>Only one NDEF message can be pushed by the currently resumed activity.
* If both {@link #setNdefPushMessage} and
- * {@link #setNdefPushMessageCallback} are set then
+ * {@link #setNdefPushMessageCallback} are set, then
* the callback will take priority.
*
- * <p>Pass a null callback to disable the callback in the
- * specified activities.
+ * <p>If neither {@link #setNdefPushMessage} or
+ * {@link #setNdefPushMessageCallback} have been called for your activity, then
+ * the Android OS may choose to send a default NDEF message on your behalf,
+ * such as a URI for your application.
*
- * <p>At least one activity must be specified, and usually only one is necessary.
+ * <p>If {@link #setNdefPushMessage} is called with a null NDEF message,
+ * and/or {@link #setNdefPushMessageCallback} is called with a null callback,
+ * then NDEF push will be completely disabled for the specified activity(s).
+ * This also disables any default NDEF message the Android OS would have
+ * otherwise sent on your behalf.
+ *
+ * <p>The API allows for multiple activities to be specified at a time,
+ * but it is strongly recommended to just register one at a time,
+ * and to do so during the activity's {@link Activity#onCreate}. For example:
+ * <pre>
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ * if (nfcAdapter == null) return; // NFC not available on this device
+ * nfcAdapter.setNdefPushMessageCallback(callback, this);
+ * }
+ * </pre>
+ * And that is it. Only one call per activity is necessary. The Android
+ * OS will automatically release its references to the callback and the
+ * Activity object when it is destroyed if you follow this pattern.
+ *
+ * <p class="note">Do not pass in an Activity that has already been through
+ * {@link Activity#onDestroy}. This is guaranteed if you call this API
+ * during {@link Activity#onCreate}.
*
* <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
*
* @param callback callback, or null to disable
- * @param activity an activity in which NDEF push should be enabled to share an NDEF message
- * that's retrieved from the provided callback
- * @param activities optional additional activities that should also enable NDEF push using
- * the provided callback
+ * @param activity activity for which the NDEF message will be pushed
+ * @param activities optional additional activities, however we strongly recommend
+ * to only register one at a time, and to do so in that activity's
+ * {@link Activity#onCreate}
*/
public void setNdefPushMessageCallback(CreateNdefMessageCallback callback, Activity activity,
Activity ... activities) {
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setNdefPushMessageCallback(activity, callback);
- for (Activity a : activities) {
- if (a == null) {
- throw new NullPointerException("activities cannot contain null");
+ int targetSdkVersion = getSdkVersion();
+ try {
+ if (activity == null) {
+ throw new NullPointerException("activity cannot be null");
+ }
+ mNfcActivityManager.setNdefPushMessageCallback(activity, callback);
+ for (Activity a : activities) {
+ if (a == null) {
+ throw new NullPointerException("activities cannot contain null");
+ }
+ mNfcActivityManager.setNdefPushMessageCallback(a, callback);
+ }
+ } catch (IllegalStateException e) {
+ if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ // Less strict on old applications - just log the error
+ Log.e(TAG, "Cannot call API with Activity that has already " +
+ "been destroyed", e);
+ } else {
+ // Prevent new applications from making this mistake, re-throw
+ throw(e);
}
- mNfcActivityManager.setNdefPushMessageCallback(a, callback);
}
}
/**
- * Set the callback on a successful NDEF push over NFC.
- *
- * <p>This method may be called at any time, but NDEF push and this callback
- * can only occur when one of the specified activities is in resumed
- * (foreground) state.
+ * Set a callback on successful Android Beam (TM).
+ *
+ * <p>This method may be called at any time before {@link Activity#onDestroy},
+ * but the callback can only occur when the
+ * specified activity(s) are in resumed (foreground) state. The recommended
+ * approach is to call this method during your Activity's
+ * {@link Activity#onCreate} - see sample
+ * code below. This method does not immediately perform any I/O or blocking work,
+ * so is safe to call on your main thread.
+ *
+ * <p>The API allows for multiple activities to be specified at a time,
+ * but it is strongly recommended to just register one at a time,
+ * and to do so during the activity's {@link Activity#onCreate}. For example:
+ * <pre>
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ * if (nfcAdapter == null) return; // NFC not available on this device
+ * nfcAdapter.setOnNdefPushCompleteCallback(callback, this);
+ * }
+ * </pre>
+ * And that is it. Only one call per activity is necessary. The Android
+ * OS will automatically release its references to the callback and the
+ * Activity object when it is destroyed if you follow this pattern.
*
- * <p>One or more activities must be specified.
+ * <p class="note">Do not pass in an Activity that has already been through
+ * {@link Activity#onDestroy}. This is guaranteed if you call this API
+ * during {@link Activity#onCreate}.
*
* <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
*
* @param callback callback, or null to disable
- * @param activity an activity to enable the callback (at least one is required)
- * @param activities zero or more additional activities to enable to callback
+ * @param activity activity for which the NDEF message will be pushed
+ * @param activities optional additional activities, however we strongly recommend
+ * to only register one at a time, and to do so in that activity's
+ * {@link Activity#onCreate}
*/
public void setOnNdefPushCompleteCallback(OnNdefPushCompleteCallback callback,
Activity activity, Activity ... activities) {
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setOnNdefPushCompleteCallback(activity, callback);
- for (Activity a : activities) {
- if (a == null) {
- throw new NullPointerException("activities cannot contain null");
+ int targetSdkVersion = getSdkVersion();
+ try {
+ if (activity == null) {
+ throw new NullPointerException("activity cannot be null");
+ }
+ mNfcActivityManager.setOnNdefPushCompleteCallback(activity, callback);
+ for (Activity a : activities) {
+ if (a == null) {
+ throw new NullPointerException("activities cannot contain null");
+ }
+ mNfcActivityManager.setOnNdefPushCompleteCallback(a, callback);
+ }
+ } catch (IllegalStateException e) {
+ if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ // Less strict on old applications - just log the error
+ Log.e(TAG, "Cannot call API with Activity that has already " +
+ "been destroyed", e);
+ } else {
+ // Prevent new applications from making this mistake, re-throw
+ throw(e);
}
- mNfcActivityManager.setOnNdefPushCompleteCallback(a, callback);
}
}
@@ -932,4 +1053,12 @@ public final class NfcAdapter {
throw new IllegalStateException("API cannot be called while activity is paused");
}
}
+
+ int getSdkVersion() {
+ if (mContext == null) {
+ return android.os.Build.VERSION_CODES.GINGERBREAD; // best guess
+ } else {
+ return mContext.getApplicationInfo().targetSdkVersion;
+ }
+ }
}
diff --git a/core/java/android/nfc/NfcFragment.java b/core/java/android/nfc/NfcFragment.java
deleted file mode 100644
index d6b15ad..0000000
--- a/core/java/android/nfc/NfcFragment.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2011 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.nfc;
-
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentManager;
-
-/**
- * Used by {@link NfcActivityManager} to attach to activity life-cycle.
- * @hide
- */
-public final class NfcFragment extends Fragment {
- static final String FRAGMENT_TAG = "android.nfc.NfcFragment";
-
- // only used on UI thread
- static boolean sIsInitialized = false;
- static NfcActivityManager sNfcActivityManager;
-
- /**
- * Attach NfcFragment to an activity (if not already attached).
- */
- public static void attach(Activity activity) {
- FragmentManager manager = activity.getFragmentManager();
- if (manager.findFragmentByTag(FRAGMENT_TAG) == null) {
- manager.beginTransaction().add(new NfcFragment(), FRAGMENT_TAG).commit();
- }
- }
-
- /**
- * Remove NfcFragment from activity.
- */
- public static void remove(Activity activity) {
- FragmentManager manager = activity.getFragmentManager();
- Fragment fragment = manager.findFragmentByTag(FRAGMENT_TAG);
- if (fragment != null) {
- // We allow state loss at this point, because the state is only
- // lost when activity is being paused *AND* subsequently destroyed.
- // In that case, the app will setup foreground dispatch again anyway.
- manager.beginTransaction().remove(fragment).commitAllowingStateLoss();
- }
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- if (!sIsInitialized) {
- sIsInitialized = true;
- NfcAdapter adapter = NfcAdapter.getDefaultAdapter(
- activity.getApplicationContext());
- if (adapter != null) {
- sNfcActivityManager = adapter.mNfcActivityManager;
- }
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (sNfcActivityManager != null) {
- sNfcActivityManager.onResume(getActivity());
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (sNfcActivityManager != null) {
- sNfcActivityManager.onPause(getActivity());
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (sNfcActivityManager != null) {
- sNfcActivityManager.onDestroy(getActivity());
- }
- }
-
-
-}
diff --git a/core/java/android/nfc/tech/Ndef.java b/core/java/android/nfc/tech/Ndef.java
index 226e079..a31cb9c 100644
--- a/core/java/android/nfc/tech/Ndef.java
+++ b/core/java/android/nfc/tech/Ndef.java
@@ -176,8 +176,11 @@ public final class Ndef extends BasicTagTechnology {
* <p>If the NDEF Message is modified by an I/O operation then it
* will not be updated here, this function only returns what was discovered
* when the tag entered the field.
+ * <p>Note that this method may return null if the tag was in the
+ * INITIALIZED state as defined by NFC Forum, as in this state the
+ * tag is formatted to support NDEF but does not contain a message yet.
* <p>Does not cause any RF activity and does not block.
- * @return NDEF Message read from the tag at discovery time
+ * @return NDEF Message read from the tag at discovery time, can be null
*/
public NdefMessage getCachedNdefMessage() {
return mNdefMsg;
@@ -245,11 +248,17 @@ public final class Ndef extends BasicTagTechnology {
*
* <p>This always reads the current NDEF Message stored on the tag.
*
+ * <p>Note that this method may return null if the tag was in the
+ * INITIALIZED state as defined by NFC Forum, as in that state the
+ * tag is formatted to support NDEF but does not contain a message yet.
+ *
* <p>This is an I/O operation and will block until complete. It must
* 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.
*
- * @return the NDEF Message, never null
+ * <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
* @throws FormatException if the NDEF Message on the tag is malformed
@@ -265,17 +274,8 @@ public final class Ndef extends BasicTagTechnology {
int serviceHandle = mTag.getServiceHandle();
if (tagService.isNdef(serviceHandle)) {
NdefMessage msg = tagService.ndefRead(serviceHandle);
- if (msg == null) {
- int errorCode = tagService.getLastError(serviceHandle);
- switch (errorCode) {
- case ErrorCodes.ERROR_IO:
- throw new IOException();
- case ErrorCodes.ERROR_INVALID_PARAM:
- throw new FormatException();
- default:
- // Should not happen
- throw new IOException();
- }
+ if (msg == null && !tagService.isPresent(serviceHandle)) {
+ throw new TagLostException();
}
return msg;
} else {
diff --git a/core/java/android/nfc/tech/NdefFormatable.java b/core/java/android/nfc/tech/NdefFormatable.java
index bb2eb94..ffa6a2b 100644
--- a/core/java/android/nfc/tech/NdefFormatable.java
+++ b/core/java/android/nfc/tech/NdefFormatable.java
@@ -137,7 +137,12 @@ public final class NdefFormatable extends BasicTagTechnology {
throw new IOException();
}
// Now check and see if the format worked
- if (tagService.isNdef(serviceHandle)) {
+ if (!tagService.isNdef(serviceHandle)) {
+ throw new IOException();
+ }
+
+ // Write a message, if one was provided
+ if (firstMessage != null) {
errorCode = tagService.ndefWrite(serviceHandle, firstMessage);
switch (errorCode) {
case ErrorCodes.SUCCESS:
@@ -150,9 +155,8 @@ public final class NdefFormatable extends BasicTagTechnology {
// Should not happen
throw new IOException();
}
- } else {
- throw new IOException();
}
+
// optionally make read-only
if (makeReadOnly) {
errorCode = tagService.ndefMakeReadOnly(serviceHandle);
diff --git a/core/java/android/service/textservice/SpellCheckerService.java b/core/java/android/service/textservice/SpellCheckerService.java
index cac449d..53ce32d 100644
--- a/core/java/android/service/textservice/SpellCheckerService.java
+++ b/core/java/android/service/textservice/SpellCheckerService.java
@@ -112,7 +112,7 @@ public abstract class SpellCheckerService extends Service {
* So, this is not called on the main thread,
* but will be called in series on another thread.
* @param textInfo the text metadata
- * @param suggestionsLimit the number of limit of suggestions returned
+ * @param suggestionsLimit the maximum number of suggestions to be returned
* @return SuggestionsInfo which contains suggestions for textInfo
*/
public abstract SuggestionsInfo onGetSuggestions(TextInfo textInfo, int suggestionsLimit);
@@ -123,9 +123,10 @@ public abstract class SpellCheckerService extends Service {
* So, this is not called on the main thread,
* but will be called in series on another thread.
* @param textInfos an array of the text metadata
- * @param suggestionsLimit the number of limit of suggestions returned
+ * @param suggestionsLimit the maximum number of suggestions to be returned
* @param sequentialWords true if textInfos can be treated as sequential words.
- * @return an array of SuggestionsInfo of onGetSuggestions
+ * @return an array of {@link SentenceSuggestionsInfo} returned by
+ * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
*/
public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
int suggestionsLimit, boolean sequentialWords) {
@@ -140,11 +141,18 @@ public abstract class SpellCheckerService extends Service {
}
/**
- * @hide
- * The default implementation returns an array of SentenceSuggestionsInfo by simply calling
- * onGetSuggestions().
+ * Get sentence suggestions for specified texts in an array of TextInfo.
+ * The default implementation returns an array of SentenceSuggestionsInfo by simply
+ * calling onGetSuggestions.
+ * This function will run on the incoming IPC thread.
+ * So, this is not called on the main thread,
+ * but will be called in series on another thread.
* When you override this method, make sure that suggestionsLimit is applied to suggestions
* that share the same start position and length.
+ * @param textInfos an array of the text metadata
+ * @param suggestionsLimit the maximum number of suggestions to be returned
+ * @return an array of {@link SentenceSuggestionsInfo} returned by
+ * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
*/
public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
int suggestionsLimit) {
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 5f2d642..ff5a467 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -378,12 +378,16 @@ public class DynamicLayout extends Layout
* An index is associated to each block (which will be used by display lists),
* this class simply invalidates the index of blocks overlapping a modification.
*
+ * This method is package private and not private so that it can be tested.
+ *
* @param startLine the first line of the range of modified lines
* @param endLine the last line of the range, possibly equal to startLine, lower
* than getLineCount()
* @param newLineCount the number of lines that will replace the range, possibly 0
+ *
+ * @hide
*/
- private void updateBlocks(int startLine, int endLine, int newLineCount) {
+ void updateBlocks(int startLine, int endLine, int newLineCount) {
int firstBlock = -1;
int lastBlock = -1;
for (int i = 0; i < mNumberOfBlocks; i++) {
@@ -466,6 +470,18 @@ public class DynamicLayout extends Layout
}
/**
+ * This package private method is used for test purposes only
+ * @hide
+ */
+ void setBlocksDataForTest(int[] blockEnds, int[] blockIndices, int numberOfBlocks) {
+ mBlockEnds = new int[blockEnds.length];
+ mBlockIndices = new int[blockIndices.length];
+ System.arraycopy(blockEnds, 0, mBlockEnds, 0, blockEnds.length);
+ System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
+ mNumberOfBlocks = numberOfBlocks;
+ }
+
+ /**
* @hide
*/
public int[] getBlockEnds() {
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index b708750..d0c87c6 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -338,7 +338,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
en = tbend;
if (getSpanStart(spans[i]) < 0) {
- setSpan(false, spans[i],
+ setSpan(true, spans[i],
st - tbstart + start,
en - tbstart + start,
sp.getSpanFlags(spans[i]));
@@ -579,8 +579,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
mSpanEnds[i] = end;
mSpanFlags[i] = flags;
- if (send)
- sendSpanChanged(what, ostart, oend, nstart, nend);
+ if (send) sendSpanChanged(what, ostart, oend, nstart, nend);
return;
}
@@ -610,8 +609,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
mSpanFlags[mSpanCount] = flags;
mSpanCount++;
- if (send)
- sendSpanAdded(what, nstart, nend);
+ if (send) sendSpanAdded(what, nstart, nend);
}
/**
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index d217cab..1cb15a6 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -65,24 +65,22 @@ public final class Choreographer {
}
};
- // System property to enable/disable vsync for animations and drawing.
- // Enabled by default.
+ // Enable/disable vsync for animations and drawing.
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
"debug.choreographer.vsync", true);
- // System property to enable/disable the use of the vsync / animation timer
- // for drawing rather than drawing immediately.
- // Temporarily disabled by default because postponing performTraversals() violates
- // assumptions about traversals happening in-order relative to other posted messages.
- // Bug: 5721047
- private static final boolean USE_ANIMATION_TIMER_FOR_DRAW = SystemProperties.getBoolean(
- "debug.choreographer.animdraw", false);
+ // Enable/disable allowing traversals to proceed immediately if no drawing occurred
+ // during the previous frame. When true, the Choreographer can degrade more gracefully
+ // if drawing takes longer than a frame, but it may potentially block in eglSwapBuffers()
+ // if there are two dirty buffers enqueued.
+ // When false, we always schedule traversals on strict vsync boundaries.
+ private static final boolean USE_PIPELINING = SystemProperties.getBoolean(
+ "debug.choreographer.pipeline", false);
- private static final int MSG_DO_ANIMATION = 0;
- private static final int MSG_DO_DRAW = 1;
- private static final int MSG_DO_SCHEDULE_VSYNC = 2;
- private static final int MSG_DO_SCHEDULE_ANIMATION = 3;
- private static final int MSG_DO_SCHEDULE_DRAW = 4;
+ private static final int MSG_DO_FRAME = 0;
+ private static final int MSG_DO_SCHEDULE_VSYNC = 1;
+ private static final int MSG_DO_SCHEDULE_CALLBACK = 2;
+ private static final int MSG_DO_TRAVERSAL = 3;
private final Object mLock = new Object();
@@ -92,20 +90,41 @@ public final class Choreographer {
private Callback mCallbackPool;
- private final CallbackQueue mAnimationCallbackQueue = new CallbackQueue();
- private final CallbackQueue mDrawCallbackQueue = new CallbackQueue();
+ private final CallbackQueue[] mCallbackQueues;
- private boolean mAnimationScheduled;
- private boolean mDrawScheduled;
- private long mLastAnimationTime;
- private long mLastDrawTime;
+ private boolean mFrameScheduled;
+ private long mLastFrameTime;
+ private boolean mDrewLastFrame;
+ private boolean mTraversalScheduled;
+
+ /**
+ * Callback type: Input callback. Runs first.
+ */
+ public static final int CALLBACK_INPUT = 0;
+
+ /**
+ * Callback type: Animation callback. Runs before traversals.
+ */
+ public static final int CALLBACK_ANIMATION = 1;
+
+ /**
+ * Callback type: Traversal callback. Handles layout and draw. Runs last
+ * after all other asynchronous messages have been handled.
+ */
+ public static final int CALLBACK_TRAVERSAL = 2;
+
+ private static final int CALLBACK_LAST = CALLBACK_TRAVERSAL;
private Choreographer(Looper looper) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
- mLastAnimationTime = Long.MIN_VALUE;
- mLastDrawTime = Long.MIN_VALUE;
+ mLastFrameTime = Long.MIN_VALUE;
+
+ mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
+ for (int i = 0; i <= CALLBACK_LAST; i++) {
+ mCallbackQueues[i] = new CallbackQueue();
+ }
}
/**
@@ -177,156 +196,142 @@ public final class Choreographer {
}
/**
- * Posts a callback to run on the next animation cycle.
+ * Posts a callback to run on the next frame.
* The callback only runs once and then is automatically removed.
*
- * @param action The callback action to run during the next animation cycle.
+ * @param callbackType The callback type.
+ * @param action The callback action to run during the next frame.
* @param token The callback token, or null if none.
*
- * @see #removeAnimationCallback
+ * @see #removeCallbacks
*/
- public void postAnimationCallback(Runnable action, Object token) {
- postAnimationCallbackDelayed(action, token, 0);
+ public void postCallback(int callbackType, Runnable action, Object token) {
+ postCallbackDelayed(callbackType, action, token, 0);
}
/**
- * Posts a callback to run on the next animation cycle following the specified delay.
+ * Posts a callback to run on the next frame following the specified delay.
* The callback only runs once and then is automatically removed.
*
- * @param action The callback action to run during the next animation cycle after
- * the specified delay.
+ * @param callbackType The callback type.
+ * @param action The callback action to run during the next frame after the specified delay.
* @param token The callback token, or null if none.
* @param delayMillis The delay time in milliseconds.
*
- * @see #removeAnimationCallback
+ * @see #removeCallback
*/
- public void postAnimationCallbackDelayed(Runnable action, Object token, long delayMillis) {
+ public void postCallbackDelayed(int callbackType,
+ Runnable action, Object token, long delayMillis) {
if (action == null) {
throw new IllegalArgumentException("action must not be null");
}
+ if (callbackType < 0 || callbackType > CALLBACK_LAST) {
+ throw new IllegalArgumentException("callbackType is invalid");
+ }
if (DEBUG) {
- Log.d(TAG, "PostAnimationCallback: " + action + ", token=" + token
+ Log.d(TAG, "PostCallback: type=" + callbackType
+ + ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) {
+ if (USE_PIPELINING && callbackType == CALLBACK_INPUT) {
+ Message msg = Message.obtain(mHandler, action);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
+ return;
+ }
+
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
- mAnimationCallbackQueue.addCallbackLocked(dueTime, action, token);
+ mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
- scheduleAnimationLocked(now);
+ if (USE_PIPELINING && callbackType == CALLBACK_TRAVERSAL) {
+ if (!mDrewLastFrame) {
+ if (DEBUG) {
+ Log.d(TAG, "Scheduling traversal immediately.");
+ }
+ if (!mTraversalScheduled) {
+ mTraversalScheduled = true;
+ Message msg = mHandler.obtainMessage(MSG_DO_TRAVERSAL);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageAtTime(msg, dueTime);
+ }
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Scheduling traversal on next frame.");
+ }
+ }
+ scheduleFrameLocked(now);
} else {
- Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_ANIMATION, action);
+ Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
+ msg.arg1 = callbackType;
+ msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
/**
- * Removes animation callbacks that have the specified action and token.
+ * Removes callbacks that have the specified action and token.
*
+ * @param callbackType The callback type.
* @param action The action property of the callbacks to remove, or null to remove
* callbacks with any action.
* @param token The token property of the callbacks to remove, or null to remove
* callbacks with any token.
*
- * @see #postAnimationCallback
- * @see #postAnimationCallbackDelayed
+ * @see #postCallback
+ * @see #postCallbackDelayed
*/
- public void removeAnimationCallbacks(Runnable action, Object token) {
- if (DEBUG) {
- Log.d(TAG, "RemoveAnimationCallbacks: " + action + ", token=" + token);
- }
-
- synchronized (mLock) {
- mAnimationCallbackQueue.removeCallbacksLocked(action, token);
- if (action != null && token == null) {
- mHandler.removeMessages(MSG_DO_SCHEDULE_ANIMATION, action);
- }
- }
- }
-
- /**
- * Posts a callback to run on the next draw cycle.
- * The callback only runs once and then is automatically removed.
- *
- * @param action The callback action to run during the next draw cycle.
- * @param token The callback token, or null if none.
- *
- * @see #removeDrawCallback
- */
- public void postDrawCallback(Runnable action, Object token) {
- postDrawCallbackDelayed(action, token, 0);
- }
-
- /**
- * Posts a callback to run on the next draw cycle following the specified delay.
- * The callback only runs once and then is automatically removed.
- *
- * @param action The callback action to run during the next animation cycle after
- * the specified delay.
- * @param token The callback token, or null if none.
- * @param delayMillis The delay time in milliseconds.
- *
- * @see #removeDrawCallback
- */
- public void postDrawCallbackDelayed(Runnable action, Object token, long delayMillis) {
- if (action == null) {
- throw new IllegalArgumentException("action must not be null");
+ public void removeCallbacks(int callbackType, Runnable action, Object token) {
+ if (callbackType < 0 || callbackType > CALLBACK_LAST) {
+ throw new IllegalArgumentException("callbackType is invalid");
}
if (DEBUG) {
- Log.d(TAG, "PostDrawCallback: " + action + ", token=" + token
- + ", delayMillis=" + delayMillis);
+ Log.d(TAG, "RemoveCallbacks: type=" + callbackType
+ + ", action=" + action + ", token=" + token);
}
synchronized (mLock) {
- final long now = SystemClock.uptimeMillis();
- final long dueTime = now + delayMillis;
- mDrawCallbackQueue.addCallbackLocked(dueTime, action, token);
- scheduleDrawLocked(now);
-
- if (dueTime <= now) {
- scheduleDrawLocked(now);
- } else {
- Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_DRAW, action);
- mHandler.sendMessageAtTime(msg, dueTime);
+ mCallbackQueues[callbackType].removeCallbacksLocked(action, token);
+ if (action != null && token == null) {
+ mHandler.removeMessages(MSG_DO_SCHEDULE_CALLBACK, action);
}
}
}
/**
- * Removes draw callbacks that have the specified action and token.
+ * Tells the choreographer that the application has actually drawn to a surface.
*
- * @param action The action property of the callbacks to remove, or null to remove
- * callbacks with any action.
- * @param token The token property of the callbacks to remove, or null to remove
- * callbacks with any token.
- *
- * @see #postDrawCallback
- * @see #postDrawCallbackDelayed
+ * It uses this information to determine whether to draw immediately or to
+ * post a draw to the next vsync because it might otherwise block.
*/
- public void removeDrawCallbacks(Runnable action, Object token) {
+ public void notifyDrawOccurred() {
if (DEBUG) {
- Log.d(TAG, "RemoveDrawCallbacks: " + action + ", token=" + token);
+ Log.d(TAG, "Draw occurred.");
}
- synchronized (mLock) {
- mDrawCallbackQueue.removeCallbacksLocked(action, token);
- if (action != null && token == null) {
- mHandler.removeMessages(MSG_DO_SCHEDULE_DRAW, action);
+ if (USE_PIPELINING) {
+ synchronized (mLock) {
+ if (!mDrewLastFrame) {
+ mDrewLastFrame = true;
+ scheduleFrameLocked(SystemClock.uptimeMillis());
+ }
}
}
}
- private void scheduleAnimationLocked(long now) {
- if (!mAnimationScheduled) {
- mAnimationScheduled = true;
+ private void scheduleFrameLocked(long now) {
+ if (!mFrameScheduled) {
+ mFrameScheduled = true;
if (USE_VSYNC) {
if (DEBUG) {
- Log.d(TAG, "Scheduling vsync for animation.");
+ Log.d(TAG, "Scheduling next frame on vsync.");
}
// If running on the Looper thread, then schedule the vsync immediately,
@@ -340,125 +345,94 @@ public final class Choreographer {
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
- final long nextAnimationTime = Math.max(mLastAnimationTime + sFrameDelay, now);
+ final long nextFrameTime = Math.max(mLastFrameTime + sFrameDelay, now);
if (DEBUG) {
- Log.d(TAG, "Scheduling animation in " + (nextAnimationTime - now) + " ms.");
+ Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
- Message msg = mHandler.obtainMessage(MSG_DO_ANIMATION);
+ Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
- mHandler.sendMessageAtTime(msg, nextAnimationTime);
+ mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
- private void scheduleDrawLocked(long now) {
- if (!mDrawScheduled) {
- mDrawScheduled = true;
- if (USE_ANIMATION_TIMER_FOR_DRAW) {
- scheduleAnimationLocked(now);
- } else {
- if (DEBUG) {
- Log.d(TAG, "Scheduling draw immediately.");
- }
- Message msg = mHandler.obtainMessage(MSG_DO_DRAW);
- msg.setAsynchronous(true);
- mHandler.sendMessageAtTime(msg, now);
- }
- }
- }
-
- void doAnimation() {
- doAnimationInner();
-
- if (USE_ANIMATION_TIMER_FOR_DRAW) {
- doDraw();
- }
- }
-
- void doAnimationInner() {
- final long start;
- Callback callbacks;
+ void doFrame(int frame) {
synchronized (mLock) {
- if (!mAnimationScheduled) {
+ if (!mFrameScheduled) {
return; // no work to do
}
- mAnimationScheduled = false;
-
- start = SystemClock.uptimeMillis();
- if (DEBUG) {
- Log.d(TAG, "Performing animation: " + Math.max(0, start - mLastAnimationTime)
- + " ms have elapsed since previous animation.");
- }
- mLastAnimationTime = start;
-
- callbacks = mAnimationCallbackQueue.extractDueCallbacksLocked(start);
+ mFrameScheduled = false;
+ mLastFrameTime = SystemClock.uptimeMillis();
+ mDrewLastFrame = false;
}
- if (callbacks != null) {
- runCallbacks(callbacks);
- synchronized (mLock) {
- recycleCallbacksLocked(callbacks);
- }
- }
+ doCallbacks(Choreographer.CALLBACK_INPUT);
+ doCallbacks(Choreographer.CALLBACK_ANIMATION);
+ doCallbacks(Choreographer.CALLBACK_TRAVERSAL);
if (DEBUG) {
- Log.d(TAG, "Animation took " + (SystemClock.uptimeMillis() - start) + " ms.");
+ Log.d(TAG, "Frame " + frame + ": Finished, took "
+ + (SystemClock.uptimeMillis() - mLastFrameTime) + " ms.");
}
}
- void doDraw() {
+ void doCallbacks(int callbackType) {
final long start;
Callback callbacks;
synchronized (mLock) {
- if (!mDrawScheduled) {
- return; // no work to do
- }
- mDrawScheduled = false;
-
start = SystemClock.uptimeMillis();
- if (DEBUG) {
- Log.d(TAG, "Performing draw: " + Math.max(0, start - mLastDrawTime)
- + " ms have elapsed since previous draw.");
- }
- mLastDrawTime = start;
+ callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(start);
- callbacks = mDrawCallbackQueue.extractDueCallbacksLocked(start);
+ if (USE_PIPELINING && callbackType == CALLBACK_TRAVERSAL && mTraversalScheduled) {
+ mTraversalScheduled = false;
+ mHandler.removeMessages(MSG_DO_TRAVERSAL);
+ }
}
if (callbacks != null) {
- runCallbacks(callbacks);
- synchronized (mLock) {
- recycleCallbacksLocked(callbacks);
+ for (Callback c = callbacks; c != null; c = c.next) {
+ if (DEBUG) {
+ Log.d(TAG, "RunCallback: type=" + callbackType
+ + ", action=" + c.action + ", token=" + c.token
+ + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
+ }
+ c.action.run();
}
- }
- if (DEBUG) {
- Log.d(TAG, "Draw took " + (SystemClock.uptimeMillis() - start) + " ms.");
+ synchronized (mLock) {
+ do {
+ final Callback next = callbacks.next;
+ recycleCallbackLocked(callbacks);
+ callbacks = next;
+ } while (callbacks != null);
+ }
}
}
void doScheduleVsync() {
synchronized (mLock) {
- if (mAnimationScheduled) {
+ if (mFrameScheduled) {
scheduleVsyncLocked();
}
}
}
- void doScheduleAnimation() {
+ void doScheduleCallback(int callbackType) {
synchronized (mLock) {
- final long now = SystemClock.uptimeMillis();
- if (mAnimationCallbackQueue.hasDueCallbacksLocked(now)) {
- scheduleAnimationLocked(now);
+ if (!mFrameScheduled) {
+ final long now = SystemClock.uptimeMillis();
+ if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
+ scheduleFrameLocked(now);
+ }
}
}
}
- void doScheduleDraw() {
+ void doTraversal() {
synchronized (mLock) {
- final long now = SystemClock.uptimeMillis();
- if (mDrawCallbackQueue.hasDueCallbacksLocked(now)) {
- scheduleDrawLocked(now);
+ if (mTraversalScheduled) {
+ mTraversalScheduled = false;
+ doCallbacks(CALLBACK_TRAVERSAL);
}
}
}
@@ -471,25 +445,6 @@ public final class Choreographer {
return Looper.myLooper() == mLooper;
}
- private void runCallbacks(Callback head) {
- while (head != null) {
- if (DEBUG) {
- Log.d(TAG, "RunCallback: " + head.action + ", token=" + head.token
- + ", waitMillis=" + (SystemClock.uptimeMillis() - head.dueTime));
- }
- head.action.run();
- head = head.next;
- }
- }
-
- private void recycleCallbacksLocked(Callback head) {
- while (head != null) {
- final Callback next = head.next;
- recycleCallbackLocked(head);
- head = next;
- }
- }
-
private Callback obtainCallbackLocked(long dueTime, Runnable action, Object token) {
Callback callback = mCallbackPool;
if (callback == null) {
@@ -519,20 +474,17 @@ public final class Choreographer {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_DO_ANIMATION:
- doAnimation();
- break;
- case MSG_DO_DRAW:
- doDraw();
+ case MSG_DO_FRAME:
+ doFrame(0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
- case MSG_DO_SCHEDULE_ANIMATION:
- doScheduleAnimation();
+ case MSG_DO_SCHEDULE_CALLBACK:
+ doScheduleCallback(msg.arg1);
break;
- case MSG_DO_SCHEDULE_DRAW:
- doScheduleDraw();
+ case MSG_DO_TRAVERSAL:
+ doTraversal();
break;
}
}
@@ -545,7 +497,7 @@ public final class Choreographer {
@Override
public void onVsync(long timestampNanos, int frame) {
- doAnimation();
+ doFrame(frame);
}
}
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java
index 9639faf..3529b8e 100644
--- a/core/java/android/view/FocusFinder.java
+++ b/core/java/android/view/FocusFinder.java
@@ -54,7 +54,7 @@ public class FocusFinder {
/**
* Find the next view to take focus in root's descendants, starting from the view
* that currently is focused.
- * @param root Contains focused
+ * @param root Contains focused. Cannot be null.
* @param focused Has focus now.
* @param direction Direction to look.
* @return The next focusable view, or null if none exists.
@@ -82,7 +82,7 @@ public class FocusFinder {
setFocusBottomRight(root);
break;
case View.FOCUS_FORWARD:
- if (focused != null && focused.isLayoutRtl()) {
+ if (root.isLayoutRtl()) {
setFocusTopLeft(root);
} else {
setFocusBottomRight(root);
@@ -94,7 +94,7 @@ public class FocusFinder {
setFocusTopLeft(root);
break;
case View.FOCUS_BACKWARD:
- if (focused != null && focused.isLayoutRtl()) {
+ if (root.isLayoutRtl()) {
setFocusBottomRight(root);
} else {
setFocusTopLeft(root);
@@ -121,7 +121,7 @@ public class FocusFinder {
/**
* Find the next view to take focus in root's descendants, searching from
* a particular rectangle in root's coordinates.
- * @param root Contains focusedRect.
+ * @param root Contains focusedRect. Cannot be null.
* @param focusedRect The starting point of the search.
* @param direction Direction to look.
* @return The next focusable view, or null if none exists.
@@ -155,10 +155,10 @@ public class FocusFinder {
final int count = focusables.size();
switch (direction) {
case View.FOCUS_FORWARD:
- return getForwardFocusable(focused, focusables, count);
+ return getForwardFocusable(root, focused, focusables, count);
case View.FOCUS_BACKWARD:
- return getBackwardFocusable(focused, focusables, count);
+ return getBackwardFocusable(root, focused, focusables, count);
}
return null;
}
@@ -201,13 +201,14 @@ public class FocusFinder {
return closest;
}
- private View getForwardFocusable(View focused, ArrayList<View> focusables, int count) {
- return (focused != null && focused.isLayoutRtl()) ?
+ private static View getForwardFocusable(ViewGroup root, View focused,
+ ArrayList<View> focusables, int count) {
+ return (root.isLayoutRtl()) ?
getPreviousFocusable(focused, focusables, count) :
getNextFocusable(focused, focusables, count);
}
- private View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
+ private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
if (focused != null) {
int position = focusables.lastIndexOf(focused);
if (position >= 0 && position + 1 < count) {
@@ -217,13 +218,14 @@ public class FocusFinder {
return focusables.get(0);
}
- private View getBackwardFocusable(View focused, ArrayList<View> focusables, int count) {
- return (focused != null && focused.isLayoutRtl()) ?
+ private static View getBackwardFocusable(ViewGroup root, View focused,
+ ArrayList<View> focusables, int count) {
+ return (root.isLayoutRtl()) ?
getNextFocusable(focused, focusables, count) :
getPreviousFocusable(focused, focusables, count);
}
- private View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
+ private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
if (focused != null) {
int position = focusables.indexOf(focused);
if (position > 0) {
@@ -353,7 +355,7 @@ public class FocusFinder {
/**
- * Do the "beams" w.r.t the given direcition's axis of rect1 and rect2 overlap?
+ * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
* @param direction the direction (up, down, left, right)
* @param rect1 The first rectangle
* @param rect2 The second rectangle
@@ -441,7 +443,7 @@ public class FocusFinder {
/**
* Find the distance on the minor axis w.r.t the direction to the nearest
- * edge of the destination rectange.
+ * edge of the destination rectangle.
* @param direction the direction (up, down, left, right)
* @param source The source rect.
* @param dest The destination rect.
diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java
index 0e96742..5b0433e 100644
--- a/core/java/android/view/GLES20Canvas.java
+++ b/core/java/android/view/GLES20Canvas.java
@@ -214,49 +214,6 @@ class GLES20Canvas extends HardwareCanvas {
private static native void nSetViewport(int renderer, int width, int height);
- /**
- * Preserves the back buffer of the current surface after a buffer swap.
- * Calling this method sets the EGL_SWAP_BEHAVIOR attribute of the current
- * surface to EGL_BUFFER_PRESERVED. Calling this method requires an EGL
- * config that supports EGL_SWAP_BEHAVIOR_PRESERVED_BIT.
- *
- * @return True if the swap behavior was successfully changed,
- * false otherwise.
- *
- * @hide
- */
- public static boolean preserveBackBuffer() {
- return nPreserveBackBuffer();
- }
-
- private static native boolean nPreserveBackBuffer();
-
- /**
- * Indicates whether the current surface preserves its back buffer
- * after a buffer swap.
- *
- * @return True, if the surface's EGL_SWAP_BEHAVIOR is EGL_BUFFER_PRESERVED,
- * false otherwise
- *
- * @hide
- */
- public static boolean isBackBufferPreserved() {
- return nIsBackBufferPreserved();
- }
-
- private static native boolean nIsBackBufferPreserved();
-
- /**
- * Disables v-sync. For performance testing only.
- *
- * @hide
- */
- public static void disableVsync() {
- nDisableVsync();
- }
-
- private static native void nDisableVsync();
-
@Override
public void onPreDraw(Rect dirty) {
if (dirty != null) {
diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java
index f251f36..133f601 100644
--- a/core/java/android/view/HardwareRenderer.java
+++ b/core/java/android/view/HardwareRenderer.java
@@ -281,12 +281,50 @@ 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.
+ * Calling this method sets the EGL_SWAP_BEHAVIOR attribute of the current
+ * surface to EGL_BUFFER_PRESERVED. Calling this method requires an EGL
+ * config that supports EGL_SWAP_BEHAVIOR_PRESERVED_BIT.
+ *
+ * @return True if the swap behavior was successfully changed,
+ * false otherwise.
+ */
+ static boolean preserveBackBuffer() {
+ return nPreserveBackBuffer();
+ }
+
+ private static native boolean nPreserveBackBuffer();
+
+ /**
+ * Indicates whether the current surface preserves its back buffer
+ * after a buffer swap.
+ *
+ * @return True, if the surface's EGL_SWAP_BEHAVIOR is EGL_BUFFER_PRESERVED,
+ * false otherwise
+ */
+ static boolean isBackBufferPreserved() {
+ return nIsBackBufferPreserved();
+ }
+
+ private static native boolean nIsBackBufferPreserved();
+
+ /**
+ * Disables v-sync. For performance testing only.
+ */
+ static void disableVsync() {
+ nDisableVsync();
+ }
+
+ private static native void nDisableVsync();
/**
* Interface used to receive callbacks whenever a view is drawn by
@@ -511,6 +549,7 @@ public abstract class HardwareRenderer {
private boolean mDestroyed;
private final Rect mRedrawClip = new Rect();
+ private final int[] mSurfaceSize = new int[2];
GlRenderer(int glVersion, boolean translucent) {
mGlVersion = glVersion;
@@ -789,7 +828,7 @@ public abstract class HardwareRenderer {
// If mDirtyRegions is set, this means we have an EGL configuration
// with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set
if (sDirtyRegions) {
- if (!(mDirtyRegionsEnabled = GLES20Canvas.preserveBackBuffer())) {
+ if (!(mDirtyRegionsEnabled = preserveBackBuffer())) {
Log.w(LOG_TAG, "Backbuffer cannot be preserved");
}
} else if (sDirtyRegionsRequested) {
@@ -799,7 +838,7 @@ public abstract class HardwareRenderer {
// want to set mDirtyRegions. We try to do this only if dirty
// regions were initially requested as part of the device
// configuration (see RENDER_DIRTY_REGIONS)
- mDirtyRegionsEnabled = GLES20Canvas.isBackBufferPreserved();
+ mDirtyRegionsEnabled = isBackBufferPreserved();
}
}
@@ -932,17 +971,28 @@ 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);
- beginFrame();
+ if (size[1] != mHeight || size[0] != mWidth) {
+ mWidth = size[0];
+ mHeight = size[1];
- onPreDraw(dirty);
+ canvas.setViewport(mWidth, mHeight);
- HardwareCanvas canvas = mCanvas;
- attachInfo.mHardwareCanvas = canvas;
+ dirty = null;
+ }
+ }
+
+ onPreDraw(dirty);
int saveCount = canvas.save();
callbacks.onHardwarePreDraw(canvas);
@@ -1215,7 +1265,7 @@ public abstract class HardwareRenderer {
void setup(int width, int height) {
super.setup(width, height);
if (mVsyncDisabled) {
- GLES20Canvas.disableVsync();
+ disableVsync();
}
}
@@ -1259,7 +1309,7 @@ public abstract class HardwareRenderer {
}
}
}
-
+
@Override
void destroyHardwareResources(View view) {
if (view != null) {
@@ -1277,7 +1327,7 @@ public abstract class HardwareRenderer {
GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS);
}
}
-
+
private static void destroyResources(View view) {
view.destroyHardwareResources();
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 2deeba6..81f3f6a 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 = true;
+ public static final boolean USE_DISPLAY_LIST_PROPERTIES = false;
/**
* 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;
}
}
}
@@ -8983,7 +9116,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
public void postOnAnimation(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
- attachInfo.mViewRootImpl.mChoreographer.postAnimationCallback(action, null);
+ attachInfo.mViewRootImpl.mChoreographer.postCallback(
+ Choreographer.CALLBACK_ANIMATION, action, null);
} else {
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
@@ -9007,8 +9141,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
public void postOnAnimationDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
- attachInfo.mViewRootImpl.mChoreographer.postAnimationCallbackDelayed(
- action, null, delayMillis);
+ attachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
+ Choreographer.CALLBACK_ANIMATION, action, null, delayMillis);
} else {
// Assume that post will succeed later
ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
@@ -9033,7 +9167,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mHandler.removeCallbacks(action);
- attachInfo.mViewRootImpl.mChoreographer.removeAnimationCallbacks(action, null);
+ attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_ANIMATION, action, null);
} else {
// Assume that post will succeed later
ViewRootImpl.getRunQueue().removeCallbacks(action);
@@ -10124,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;
@@ -12239,8 +12374,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
if (verifyDrawable(who) && what != null) {
final long delay = when - SystemClock.uptimeMillis();
if (mAttachInfo != null) {
- mAttachInfo.mViewRootImpl.mChoreographer.postAnimationCallbackDelayed(
- what, who, Choreographer.subtractFrameDelay(delay));
+ mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
+ Choreographer.CALLBACK_ANIMATION, what, who,
+ Choreographer.subtractFrameDelay(delay));
} else {
ViewRootImpl.getRunQueue().postDelayed(what, delay);
}
@@ -12256,7 +12392,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
public void unscheduleDrawable(Drawable who, Runnable what) {
if (verifyDrawable(who) && what != null) {
if (mAttachInfo != null) {
- mAttachInfo.mViewRootImpl.mChoreographer.removeAnimationCallbacks(what, who);
+ mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_ANIMATION, what, who);
} else {
ViewRootImpl.getRunQueue().removeCallbacks(what);
}
@@ -12274,7 +12411,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
*/
public void unscheduleDrawable(Drawable who) {
if (mAttachInfo != null && who != null) {
- mAttachInfo.mViewRootImpl.mChoreographer.removeAnimationCallbacks(null, who);
+ mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_ANIMATION, null, who);
}
}
@@ -13977,6 +14115,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 befc1c6..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;
@@ -877,50 +892,6 @@ public final class ViewRootImpl implements ViewParent,
public void bringChildToFront(View child) {
}
- public void scheduleTraversals() {
- if (!mTraversalScheduled) {
- mTraversalScheduled = true;
- mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
- scheduleFrame();
- }
- }
-
- public void unscheduleTraversals() {
- if (mTraversalScheduled) {
- mTraversalScheduled = false;
- mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
- }
- }
-
- void scheduleFrame() {
- if (!mFrameScheduled) {
- mFrameScheduled = true;
- mChoreographer.postDrawCallback(mFrameRunnable, null);
- }
- }
-
- void unscheduleFrame() {
- unscheduleTraversals();
-
- if (mFrameScheduled) {
- mFrameScheduled = false;
- mChoreographer.removeDrawCallbacks(mFrameRunnable, null);
- }
- }
-
- void doFrame() {
- if (mInputEventReceiver != null) {
- mInputEventReceiver.consumeBatchedInputEvents();
- }
- doProcessInputEvents();
-
- if (mTraversalScheduled) {
- mTraversalScheduled = false;
- mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
- doTraversal();
- }
- }
-
int getHostVisibility() {
return mAppVisible ? mView.getVisibility() : View.GONE;
}
@@ -954,42 +925,162 @@ public final class ViewRootImpl implements ViewParent,
}
}
- private void doTraversal() {
- if (mProfile) {
- Debug.startMethodTracing("ViewAncestor");
+ void scheduleTraversals() {
+ if (!mTraversalScheduled) {
+ mTraversalScheduled = true;
+ mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
+ mChoreographer.postCallback(
+ Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
+ }
- final long traversalStartTime;
- if (ViewDebug.DEBUG_LATENCY) {
- traversalStartTime = System.nanoTime();
- if (mLastTraversalFinishedTimeNanos != 0) {
- Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals(); it has been "
- + ((traversalStartTime - mLastTraversalFinishedTimeNanos) * 0.000001f)
- + "ms since the last traversals finished.");
- } else {
- Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals().");
+ void unscheduleTraversals() {
+ if (mTraversalScheduled) {
+ mTraversalScheduled = false;
+ mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
+ mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
+ }
+ }
+
+ void doTraversal() {
+ if (mTraversalScheduled) {
+ mTraversalScheduled = false;
+ mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
+
+ doConsumeBatchedInput(false);
+ doProcessInputEvents();
+
+ if (mProfile) {
+ Debug.startMethodTracing("ViewAncestor");
+ }
+
+ final long traversalStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ traversalStartTime = System.nanoTime();
+ if (mLastTraversalFinishedTimeNanos != 0) {
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals(); it has been "
+ + ((traversalStartTime - mLastTraversalFinishedTimeNanos) * 0.000001f)
+ + "ms since the last traversals finished.");
+ } else {
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals().");
+ }
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
+ try {
+ performTraversals();
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performTraversals() took "
+ + ((now - traversalStartTime) * 0.000001f)
+ + "ms.");
+ mLastTraversalFinishedTimeNanos = now;
+ }
+
+ if (mProfile) {
+ Debug.stopMethodTracing();
+ mProfile = false;
}
}
+ }
- Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
- try {
- performTraversals();
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ 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;
+ }
- if (ViewDebug.DEBUG_LATENCY) {
- long now = System.nanoTime();
- Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performTraversals() took "
- + ((now - traversalStartTime) * 0.000001f)
- + "ms.");
- mLastTraversalFinishedTimeNanos = now;
+ 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 (mProfile) {
- Debug.stopMethodTracing();
- mProfile = false;
+ 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() {
@@ -1013,8 +1104,6 @@ public final class ViewRootImpl implements ViewParent,
int desiredWindowWidth;
int desiredWindowHeight;
- int childWidthMeasureSpec;
- int childHeightMeasureSpec;
final View.AttachInfo attachInfo = mAttachInfo;
@@ -1075,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();
@@ -1111,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);
@@ -1152,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;
@@ -1263,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)
@@ -1303,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);
@@ -1320,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()
@@ -1341,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 &&
@@ -1408,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: "
@@ -1565,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()
@@ -1604,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) {
@@ -1937,6 +1975,7 @@ public final class ViewRootImpl implements ViewParent,
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
+ mChoreographer.notifyDrawOccurred();
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
@@ -2460,7 +2499,7 @@ public final class ViewRootImpl implements ViewParent,
mInputChannel = null;
}
- unscheduleFrame();
+ unscheduleTraversals();
}
void updateConfiguration(Configuration config, boolean force) {
@@ -3596,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);
@@ -3846,6 +3882,7 @@ public final class ViewRootImpl implements ViewParent,
Message msg = mHandler.obtainMessage(MSG_IME_FINISHED_EVENT);
msg.arg1 = seq;
msg.arg2 = handled ? 1 : 0;
+ msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
@@ -3971,11 +4008,13 @@ public final class ViewRootImpl implements ViewParent,
private void scheduleProcessInputEvents() {
if (!mProcessInputEventsScheduled) {
mProcessInputEventsScheduled = true;
- mHandler.sendEmptyMessage(MSG_PROCESS_INPUT_EVENTS);
+ Message msg = mHandler.obtainMessage(MSG_PROCESS_INPUT_EVENTS);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
}
}
- private void doProcessInputEvents() {
+ void doProcessInputEvents() {
while (mCurrentInputEvent == null && mFirstPendingInputEvent != null) {
QueuedInputEvent q = mFirstPendingInputEvent;
mFirstPendingInputEvent = q.mNext;
@@ -4047,15 +4086,46 @@ public final class ViewRootImpl implements ViewParent,
}
}
- final class FrameRunnable implements Runnable {
+ void scheduleConsumeBatchedInput() {
+ if (!mConsumeBatchedInputScheduled) {
+ mConsumeBatchedInputScheduled = true;
+ mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
+ mConsumedBatchedInputRunnable, null);
+ }
+ }
+
+ void unscheduleConsumeBatchedInput() {
+ if (mConsumeBatchedInputScheduled) {
+ mConsumeBatchedInputScheduled = false;
+ mChoreographer.removeCallbacks(Choreographer.CALLBACK_INPUT,
+ mConsumedBatchedInputRunnable, null);
+ }
+ }
+
+ void doConsumeBatchedInput(boolean callback) {
+ if (mConsumeBatchedInputScheduled) {
+ mConsumeBatchedInputScheduled = false;
+ if (!callback) {
+ mChoreographer.removeCallbacks(Choreographer.CALLBACK_INPUT,
+ mConsumedBatchedInputRunnable, null);
+ }
+ }
+
+ // 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();
+ }
+ }
+
+ final class TraversalRunnable implements Runnable {
@Override
public void run() {
- mFrameScheduled = false;
- doFrame();
+ doTraversal();
}
}
- final FrameRunnable mFrameRunnable = new FrameRunnable();
- boolean mFrameScheduled;
+ final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
@@ -4069,11 +4139,28 @@ public final class ViewRootImpl implements ViewParent,
@Override
public void onBatchedInputEventPending() {
- scheduleFrame();
+ scheduleConsumeBatchedInput();
+ }
+
+ @Override
+ public void dispose() {
+ unscheduleConsumeBatchedInput();
+ super.dispose();
}
}
WindowInputEventReceiver mInputEventReceiver;
+ final class ConsumeBatchedInputRunnable implements Runnable {
+ @Override
+ public void run() {
+ doConsumeBatchedInput(true);
+ doProcessInputEvents();
+ }
+ }
+ final ConsumeBatchedInputRunnable mConsumedBatchedInputRunnable =
+ new ConsumeBatchedInputRunnable();
+ boolean mConsumeBatchedInputScheduled;
+
final class InvalidateOnAnimationRunnable implements Runnable {
private boolean mPosted;
private ArrayList<View> mViews = new ArrayList<View>();
@@ -4109,7 +4196,7 @@ public final class ViewRootImpl implements ViewParent,
}
if (mPosted && mViews.isEmpty() && mViewRects.isEmpty()) {
- mChoreographer.removeAnimationCallbacks(this, null);
+ mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = false;
}
}
@@ -4150,7 +4237,7 @@ public final class ViewRootImpl implements ViewParent,
private void postIfNeededLocked() {
if (!mPosted) {
- mChoreographer.postAnimationCallback(this, null);
+ mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = true;
}
}
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/textservice/SentenceSuggestionsInfo.java b/core/java/android/view/textservice/SentenceSuggestionsInfo.java
index cb9e496..afd62eb 100644
--- a/core/java/android/view/textservice/SentenceSuggestionsInfo.java
+++ b/core/java/android/view/textservice/SentenceSuggestionsInfo.java
@@ -22,8 +22,13 @@ import android.os.Parcelable;
import java.util.Arrays;
/**
- * @hide
- * This class contains a metadata of sentence level suggestions from the text service
+ * This class contains a metadata of suggestions returned from a text service
+ * (e.g. {@link android.service.textservice.SpellCheckerService}).
+ * The text service uses this class to return the suggestions
+ * for a sentence. See {@link SuggestionsInfo} which is used for suggestions for a word.
+ * This class extends the functionality of {@link SuggestionsInfo} as far as this class enables
+ * you to put multiple {@link SuggestionsInfo}s on a sentence with the offsets and the lengths
+ * of all {@link SuggestionsInfo}s.
*/
public final class SentenceSuggestionsInfo implements Parcelable {
@@ -82,14 +87,15 @@ public final class SentenceSuggestionsInfo implements Parcelable {
}
/**
- * @hide
+ * @return the count of {@link SuggestionsInfo}s this instance holds.
*/
public int getSuggestionsCount() {
return mSuggestionsInfos.length;
}
/**
- * @hide
+ * @param i the id of {@link SuggestionsInfo}s this instance holds.
+ * @return a {@link SuggestionsInfo} at the specified id
*/
public SuggestionsInfo getSuggestionsInfoAt(int i) {
if (i >= 0 && i < mSuggestionsInfos.length) {
@@ -99,7 +105,8 @@ public final class SentenceSuggestionsInfo implements Parcelable {
}
/**
- * @hide
+ * @param i the id of {@link SuggestionsInfo}s this instance holds
+ * @return the offset of the specified {@link SuggestionsInfo}
*/
public int getOffsetAt(int i) {
if (i >= 0 && i < mOffsets.length) {
@@ -109,7 +116,8 @@ public final class SentenceSuggestionsInfo implements Parcelable {
}
/**
- * @hide
+ * @param i the id of {@link SuggestionsInfo}s this instance holds
+ * @return the length of the specified {@link SuggestionsInfo}
*/
public int getLengthAt(int i) {
if (i >= 0 && i < mLengths.length) {
diff --git a/core/java/android/view/textservice/SpellCheckerSession.java b/core/java/android/view/textservice/SpellCheckerSession.java
index 6ff3b9b..35940ba 100644
--- a/core/java/android/view/textservice/SpellCheckerSession.java
+++ b/core/java/android/view/textservice/SpellCheckerSession.java
@@ -178,17 +178,19 @@ public class SpellCheckerSession {
}
/**
- * @hide
+ * Get suggestions from the specified sentences
+ * @param textInfos an array of text metadata for a spell checker
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
*/
- public void getSentenceSuggestions(TextInfo[] textInfo, int suggestionsLimit) {
+ public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple(
- textInfo, suggestionsLimit);
+ textInfos, suggestionsLimit);
}
/**
* Get candidate strings for a substring of the specified text.
* @param textInfo text metadata for a spell checker
- * @param suggestionsLimit the number of limit of suggestions returned
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
*/
public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
@@ -197,7 +199,7 @@ public class SpellCheckerSession {
/**
* A batch process of getSuggestions
* @param textInfos an array of text metadata for a spell checker
- * @param suggestionsLimit the number of limit of suggestions returned
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
* @param sequentialWords true if textInfos can be treated as sequential words.
*/
public void getSuggestions(
@@ -434,12 +436,19 @@ public class SpellCheckerSession {
*/
public interface SpellCheckerSessionListener {
/**
- * Callback for "getSuggestions"
- * @param results an array of results of getSuggestions
+ * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
+ * @param results an array of {@link SuggestionsInfo}s.
+ * These results are suggestions for {@link TextInfo}s queried by
+ * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}.
*/
public void onGetSuggestions(SuggestionsInfo[] results);
+ // TODO: Remove @hide as soon as the sample spell checker client gets fixed.
/**
* @hide
+ * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
+ * @param results an array of {@link SentenceSuggestionsInfo}s.
+ * These results are suggestions for {@link TextInfo}s
+ * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
*/
public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
}
diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java
index d7b6adb..93eb082 100755
--- a/core/java/android/webkit/GeolocationPermissions.java
+++ b/core/java/android/webkit/GeolocationPermissions.java
@@ -18,14 +18,12 @@ package android.webkit;
import android.os.Handler;
import android.os.Message;
-import android.util.Log;
+
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
-
/**
* This class is used to manage permissions for the WebView's Geolocation
* JavaScript API.
@@ -47,7 +45,7 @@ import java.util.Vector;
* Geolocation permissions at any time.
*/
// This class is the Java counterpart of the WebKit C++ GeolocationPermissions
-// class. It simply marshalls calls from the UI thread to the WebKit thread.
+// class. It simply marshals calls from the UI thread to the WebKit thread.
//
// Within WebKit, Geolocation permissions may be applied either temporarily
// (for the duration of the page) or permanently. This class deals only with
@@ -70,9 +68,6 @@ public final class GeolocationPermissions {
public void invoke(String origin, boolean allow, boolean retain);
};
- // Log tag
- private static final String TAG = "geolocationPermissions";
-
// Global instance
private static GeolocationPermissions sInstance;
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/WebStorage.java b/core/java/android/webkit/WebStorage.java
index c079404..2300c2e 100644
--- a/core/java/android/webkit/WebStorage.java
+++ b/core/java/android/webkit/WebStorage.java
@@ -18,13 +18,10 @@ package android.webkit;
import android.os.Handler;
import android.os.Message;
-import android.util.Log;
import java.util.Collection;
-import java.util.Map;
import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Map;
import java.util.Set;
/**
@@ -44,9 +41,6 @@ public final class WebStorage {
public void updateQuota(long newQuota);
};
- // Log tag
- private static final String TAG = "webstorage";
-
// Global instance of a WebStorage
private static WebStorage sWebStorage;
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 7ddff8e..04fa07a 100644
--- a/core/java/android/webkit/WebViewClassic.java
+++ b/core/java/android/webkit/WebViewClassic.java
@@ -70,8 +70,6 @@ import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
import android.view.Display;
-import android.view.GestureDetector;
-import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.HardwareCanvas;
@@ -386,6 +384,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
private boolean mIsAutoFillable;
private boolean mIsAutoCompleteEnabled;
private String mName;
+ private int mBatchLevel;
public WebViewInputConnection() {
super(mWebView, true);
@@ -404,6 +403,24 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
}
+ @Override
+ public boolean beginBatchEdit() {
+ if (mBatchLevel == 0) {
+ beginTextBatch();
+ }
+ mBatchLevel++;
+ return false;
+ }
+
+ @Override
+ public boolean endBatchEdit() {
+ mBatchLevel--;
+ if (mBatchLevel == 0) {
+ commitTextBatch();
+ }
+ return false;
+ }
+
public boolean getIsAutoFillable() {
return mIsAutoFillable;
}
@@ -801,51 +818,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
}
- private class TextScrollListener extends SimpleOnGestureListener {
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2,
- float velocityX, float velocityY) {
- int maxScrollX = mEditTextContent.width() -
- mEditTextBounds.width();
- int maxScrollY = mEditTextContent.height() -
- mEditTextBounds.height();
-
- int contentVelocityX = viewToContentDimension((int)-velocityX);
- int contentVelocityY = viewToContentDimension((int)-velocityY);
- mEditTextScroller.fling(-mEditTextContent.left,
- -mEditTextContent.top,
- contentVelocityX, contentVelocityY,
- 0, maxScrollX, 0, maxScrollY);
- return true;
- }
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2,
- float distanceX, float distanceY) {
- // Scrollable edit text. Scroll it.
- int newScrollX = deltaToTextScroll(
- -mEditTextContent.left, mEditTextContent.width(),
- mEditTextBounds.width(),
- (int) distanceX);
- int newScrollY = deltaToTextScroll(
- -mEditTextContent.top, mEditTextContent.height(),
- mEditTextBounds.height(),
- (int) distanceY);
- scrollEditText(newScrollX, newScrollY);
- return true;
- }
-
- private int deltaToTextScroll(int oldScroll, int contentSize,
- int boundsSize, int delta) {
- int newScroll = oldScroll +
- viewToContentDimension(delta);
- int maxScroll = contentSize - boundsSize;
- newScroll = Math.min(maxScroll, newScroll);
- newScroll = Math.max(0, newScroll);
- return newScroll;
- }
- }
-
// The listener to capture global layout change event.
private InnerGlobalLayoutListener mGlobalLayoutListener = null;
@@ -874,11 +846,12 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
private int mFieldPointer;
private PastePopupWindow mPasteWindow;
private AutoCompletePopup mAutoCompletePopup;
- private GestureDetector mGestureDetector;
Rect mEditTextBounds = new Rect();
Rect mEditTextContent = new Rect();
int mEditTextLayerId;
boolean mIsEditingText = false;
+ ArrayList<Message> mBatchedTextChanges = new ArrayList<Message>();
+ boolean mIsBatchingTextChanges = false;
private static class OnTrimMemoryListener implements ComponentCallbacks2 {
private static OnTrimMemoryListener sInstance = null;
@@ -1016,6 +989,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
private static final int TOUCH_DONE_MODE = 7;
private static final int TOUCH_PINCH_DRAG = 8;
private static final int TOUCH_DRAG_LAYER_MODE = 9;
+ private static final int TOUCH_DRAG_TEXT_MODE = 10;
// Whether to forward the touch events to WebCore
// Can only be set by WebKit via JNI.
@@ -1446,6 +1420,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
*/
@@ -1493,7 +1470,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
mAutoFillData = new WebViewCore.AutoFillData();
- mGestureDetector = new GestureDetector(mContext, new TextScrollListener());
mEditTextScroller = new Scroller(context);
}
@@ -1895,11 +1871,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);
@@ -1921,34 +1903,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
@@ -3383,6 +3377,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
boolean clampedY) {
// Special-case layer scrolling so that we do not trigger normal scroll
// updating.
+ if (mTouchMode == TOUCH_DRAG_TEXT_MODE) {
+ scrollEditText(scrollX, scrollY);
+ return;
+ }
if (mTouchMode == TOUCH_DRAG_LAYER_MODE) {
scrollLayerTo(scrollX, scrollY);
return;
@@ -3855,6 +3853,12 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
rangeY = mScrollingLayerRect.bottom;
// No overscrolling for layers.
overflingDistance = 0;
+ } else if (mTouchMode == TOUCH_DRAG_TEXT_MODE) {
+ oldX = getTextScrollX();
+ oldY = getTextScrollY();
+ rangeX = getMaxTextScrollX();
+ rangeY = getMaxTextScrollY();
+ overflingDistance = 0;
}
mWebViewPrivate.overScrollBy(x - oldX, y - oldY, oldX, oldY,
@@ -3865,12 +3869,14 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
mOverScrollGlow.absorbGlow(x, y, oldX, oldY, rangeX, rangeY);
}
} else {
- if (mTouchMode != TOUCH_DRAG_LAYER_MODE) {
- setScrollXRaw(x);
- setScrollYRaw(y);
- } else {
+ if (mTouchMode == TOUCH_DRAG_LAYER_MODE) {
// Update the layer position instead of WebView.
scrollLayerTo(x, y);
+ } else if (mTouchMode == TOUCH_DRAG_TEXT_MODE) {
+ scrollEditText(x, y);
+ } else {
+ setScrollXRaw(x);
+ setScrollYRaw(y);
}
abortAnimation();
nativeSetIsScrolling(false);
@@ -3892,7 +3898,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) {
@@ -5095,8 +5101,8 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
// send complex characters to webkit for use by JS and plugins
if (keyCode == KeyEvent.KEYCODE_UNKNOWN && event.getCharacters() != null) {
// pass the key to DOM
- mWebViewCore.sendMessage(EventHub.KEY_DOWN, event);
- mWebViewCore.sendMessage(EventHub.KEY_UP, event);
+ sendBatchableInputMessage(EventHub.KEY_DOWN, 0, 0, event);
+ sendBatchableInputMessage(EventHub.KEY_UP, 0, 0, event);
// return true as DOM handles the key
return true;
}
@@ -5162,7 +5168,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
// if an accessibility script is injected we delegate to it the key handling.
// this script is a screen reader which is a fully fledged solution for blind
// users to navigate in and interact with web pages.
- mWebViewCore.sendMessage(EventHub.KEY_DOWN, event);
+ sendBatchableInputMessage(EventHub.KEY_DOWN, 0, 0, event);
return true;
} else {
// Clean up if accessibility was disabled after loading the current URL.
@@ -5289,7 +5295,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
// if an accessibility script is injected we delegate to it the key handling.
// this script is a screen reader which is a fully fledged solution for blind
// users to navigate in and interact with web pages.
- mWebViewCore.sendMessage(EventHub.KEY_UP, event);
+ sendBatchableInputMessage(EventHub.KEY_UP, 0, 0, event);
return true;
} else {
// Clean up if accessibility was disabled after loading the current URL.
@@ -6156,7 +6162,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
startTouch(x, y, eventTime);
if (mIsEditingText) {
mTouchInEditText = mEditTextBounds.contains(contentX, contentY);
- mGestureDetector.onTouchEvent(ev);
}
break;
}
@@ -6189,13 +6194,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
invalidate();
}
break;
- } else if (mConfirmMove && mTouchInEditText) {
- ViewParent parent = mWebView.getParent();
- if (parent != null) {
- parent.requestDisallowInterceptTouchEvent(true);
- }
- mGestureDetector.onTouchEvent(ev);
- break;
}
// pass the touch events from UI thread to WebCore thread
@@ -6243,7 +6241,8 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
if (mTouchMode != TOUCH_DRAG_MODE &&
- mTouchMode != TOUCH_DRAG_LAYER_MODE) {
+ mTouchMode != TOUCH_DRAG_LAYER_MODE &&
+ mTouchMode != TOUCH_DRAG_TEXT_MODE) {
if (!mConfirmMove) {
break;
@@ -6326,9 +6325,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
deltaX = 0;
}
}
- mLastTouchX = x;
- mLastTouchY = y;
-
if (deltaX * deltaX + deltaY * deltaY > mTouchSlopSquare) {
mHeldMotionless = MOTIONLESS_FALSE;
nativeSetIsScrolling(true);
@@ -6339,13 +6335,24 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
mLastTouchTime = eventTime;
+ boolean allDrag = doDrag(deltaX, deltaY);
+ if (allDrag) {
+ mLastTouchX = x;
+ mLastTouchY = y;
+ } else {
+ int contentDeltaX = (int)Math.floor(deltaX * mZoomManager.getInvScale());
+ int roundedDeltaX = contentToViewDimension(contentDeltaX);
+ int contentDeltaY = (int)Math.floor(deltaY * mZoomManager.getInvScale());
+ int roundedDeltaY = contentToViewDimension(contentDeltaY);
+ mLastTouchX -= roundedDeltaX;
+ mLastTouchY -= roundedDeltaY;
+ }
}
- doDrag(deltaX, deltaY);
-
// Turn off scrollbars when dragging a layer.
if (keepScrollBarsVisible &&
- mTouchMode != TOUCH_DRAG_LAYER_MODE) {
+ mTouchMode != TOUCH_DRAG_LAYER_MODE &&
+ mTouchMode != TOUCH_DRAG_TEXT_MODE) {
if (mHeldMotionless != MOTIONLESS_TRUE) {
mHeldMotionless = MOTIONLESS_TRUE;
invalidate();
@@ -6366,11 +6373,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
break;
}
case MotionEvent.ACTION_UP: {
- mGestureDetector.onTouchEvent(ev);
- if (mTouchInEditText && mConfirmMove) {
- stopTouch();
- break; // We've been scrolling the edit text.
- }
if (!mConfirmMove && mIsEditingText && mSelectionStarted &&
mIsCaretSelection) {
showPasteWindow();
@@ -6484,6 +6486,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
case TOUCH_DRAG_MODE:
case TOUCH_DRAG_LAYER_MODE:
+ case TOUCH_DRAG_TEXT_MODE:
mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS);
mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS);
// if the user waits a while w/o moving before the
@@ -6680,20 +6683,31 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
}
- private void doDrag(int deltaX, int deltaY) {
+ private boolean doDrag(int deltaX, int deltaY) {
+ boolean allDrag = true;
if ((deltaX | deltaY) != 0) {
int oldX = getScrollX();
int oldY = getScrollY();
int rangeX = computeMaxScrollX();
int rangeY = computeMaxScrollY();
- // Check for the original scrolling layer in case we change
- // directions. mTouchMode might be TOUCH_DRAG_MODE if we have
- // reached the edge of a layer but mScrollingLayer will be non-zero
- // if we initiated the drag on a layer.
- if (mCurrentScrollingLayerId != 0) {
- final int contentX = viewToContentDimension(deltaX);
- final int contentY = viewToContentDimension(deltaY);
-
+ final int contentX = (int)Math.floor(deltaX * mZoomManager.getInvScale());
+ final int contentY = (int)Math.floor(deltaY * mZoomManager.getInvScale());
+
+ // Assume page scrolling and change below if we're wrong
+ mTouchMode = TOUCH_DRAG_MODE;
+
+ // Check special scrolling before going to main page scrolling.
+ if (mIsEditingText && mTouchInEditText && canTextScroll(deltaX, deltaY)) {
+ // Edit text scrolling
+ oldX = getTextScrollX();
+ rangeX = getMaxTextScrollX();
+ deltaX = contentX;
+ oldY = getTextScrollY();
+ rangeY = getMaxTextScrollY();
+ deltaY = contentY;
+ mTouchMode = TOUCH_DRAG_TEXT_MODE;
+ allDrag = false;
+ } else if (mCurrentScrollingLayerId != 0) {
// Check the scrolling bounds to see if we will actually do any
// scrolling. The rectangle is in document coordinates.
final int maxX = mScrollingLayerRect.right;
@@ -6713,12 +6727,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
oldY = mScrollingLayerRect.top;
rangeX = maxX;
rangeY = maxY;
- } else {
- // Scroll the main page if we are not going to scroll the
- // layer. This does not reset mScrollingLayer in case the
- // user changes directions and the layer can scroll the
- // other way.
- mTouchMode = TOUCH_DRAG_MODE;
+ allDrag = false;
}
}
@@ -6734,11 +6743,13 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
}
}
mZoomManager.keepZoomPickerVisible();
+ return allDrag;
}
private void stopTouch() {
if (mScroller.isFinished() && !mSelectingText
- && (mTouchMode == TOUCH_DRAG_MODE || mTouchMode == TOUCH_DRAG_LAYER_MODE)) {
+ && (mTouchMode == TOUCH_DRAG_MODE
+ || mTouchMode == TOUCH_DRAG_LAYER_MODE)) {
WebViewCore.resumePriority();
WebViewCore.resumeUpdatePicture(mWebViewCore);
nativeSetIsScrolling(false);
@@ -7132,6 +7143,13 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
maxY = mScrollingLayerRect.bottom;
// No overscrolling for layers.
overscrollDistance = overflingDistance = 0;
+ } else if (mTouchMode == TOUCH_DRAG_TEXT_MODE) {
+ scrollX = getTextScrollX();
+ scrollY = getTextScrollY();
+ maxX = getMaxTextScrollX();
+ maxY = getMaxTextScrollY();
+ // No overscrolling for edit text.
+ overscrollDistance = overflingDistance = 0;
}
if (mSnapScrollMode != SNAP_NONE) {
@@ -7211,7 +7229,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
final int time = mScroller.getDuration();
// Suppress scrollbars for layer scrolling.
- if (mTouchMode != TOUCH_DRAG_LAYER_MODE) {
+ if (mTouchMode != TOUCH_DRAG_LAYER_MODE && mTouchMode != TOUCH_DRAG_TEXT_MODE) {
mWebViewPrivate.awakenScrollBars(time);
}
@@ -7543,7 +7561,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
arg.mNewEnd = newEnd;
mTextGeneration++;
arg.mTextGeneration = mTextGeneration;
- mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg);
+ sendBatchableInputMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg);
}
/* package */ void passToJavaScript(String currentText, KeyEvent event) {
@@ -7569,6 +7587,36 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
return mWebViewCore;
}
+ private boolean canTextScroll(int directionX, int directionY) {
+ int scrollX = getTextScrollX();
+ int scrollY = getTextScrollY();
+ int maxScrollX = getMaxTextScrollX();
+ int maxScrollY = getMaxTextScrollY();
+ boolean canScrollX = (directionX > 0)
+ ? (scrollX < maxScrollX)
+ : (scrollX > 0);
+ boolean canScrollY = (directionY > 0)
+ ? (scrollY < maxScrollY)
+ : (scrollY > 0);
+ return canScrollX || canScrollY;
+ }
+
+ private int getTextScrollX() {
+ return -mEditTextContent.left;
+ }
+
+ private int getTextScrollY() {
+ return -mEditTextContent.top;
+ }
+
+ private int getMaxTextScrollX() {
+ return Math.max(0, mEditTextContent.width() - mEditTextBounds.width());
+ }
+
+ private int getMaxTextScrollY() {
+ return Math.max(0, mEditTextContent.height() - mEditTextBounds.height());
+ }
+
/**
* Used only by TouchEventQueue to store pending touch events.
*/
@@ -8512,7 +8560,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
break;
case KEY_PRESS:
- mWebViewCore.sendMessage(EventHub.KEY_PRESS, msg.arg1);
+ sendBatchableInputMessage(EventHub.KEY_PRESS, msg.arg1, 0, null);
break;
case RELOCATE_AUTO_COMPLETE_POPUP:
@@ -8888,8 +8936,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
private void scrollEditText(int scrollX, int scrollY) {
// Scrollable edit text. Scroll it.
- float maxScrollX = (float)(mEditTextContent.width() -
- mEditTextBounds.width());
+ float maxScrollX = getMaxTextScrollX();
float scrollPercentX = ((float)scrollX)/maxScrollX;
mEditTextContent.offsetTo(-scrollX, -scrollY);
mWebViewCore.sendMessageAtFrontOfQueue(EventHub.SCROLL_TEXT_INPUT, 0,
@@ -8898,6 +8945,31 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
TEXT_SCROLL_ANIMATION_DELAY_MS);
}
+ private void beginTextBatch() {
+ mIsBatchingTextChanges = true;
+ }
+
+ private void commitTextBatch() {
+ if (mWebViewCore != null) {
+ mWebViewCore.sendMessages(mBatchedTextChanges);
+ }
+ mBatchedTextChanges.clear();
+ mIsBatchingTextChanges = false;
+ }
+
+ private void sendBatchableInputMessage(int what, int arg1, int arg2,
+ Object obj) {
+ if (mWebViewCore == null) {
+ return;
+ }
+ Message message = Message.obtain(null, what, arg1, arg2, obj);
+ if (mIsBatchingTextChanges) {
+ mBatchedTextChanges.add(message);
+ } else {
+ mWebViewCore.sendMessage(message);
+ }
+ }
+
// Class used to use a dropdown for a <select> element
private class InvokeListBox implements Runnable {
// Whether the listbox allows multiple selection.
@@ -9296,7 +9368,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
mWebView.playSoundEffect(sound);
}
}
- mWebViewCore.sendMessage(eventHubAction, direction, event);
+ sendBatchableInputMessage(eventHubAction, direction, 0, event);
}
/**
diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java
index afb2992..3eba6d7 100644
--- a/core/java/android/webkit/WebViewCore.java
+++ b/core/java/android/webkit/WebViewCore.java
@@ -1914,6 +1914,14 @@ public final class WebViewCore {
mEventHub.sendMessage(msg);
}
+ void sendMessages(ArrayList<Message> messages) {
+ synchronized (mEventHub) {
+ for (int i = 0; i < messages.size(); i++) {
+ mEventHub.sendMessage(messages.get(i));
+ }
+ }
+ }
+
void sendMessage(int what) {
mEventHub.sendMessage(Message.obtain(null, what));
}
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
new file mode 100644
index 0000000..880dc34
--- /dev/null
+++ b/core/java/android/widget/Editor.java
@@ -0,0 +1,3750 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.R;
+import android.content.ClipData;
+import android.content.ClipData.Item;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.inputmethodservice.ExtractEditText;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.KeyListener;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.WordIterator;
+import android.text.style.EasyEditSpan;
+import android.text.style.SuggestionRangeSpan;
+import android.text.style.SuggestionSpan;
+import android.text.style.TextAppearanceSpan;
+import android.text.style.URLSpan;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.DisplayList;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.HardwareCanvas;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.View.DragShadowBuilder;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView.Drawables;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.EditableInputConnection;
+
+import java.text.BreakIterator;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+/**
+ * Helper class used by TextView to handle editable text views.
+ *
+ * @hide
+ */
+public class Editor {
+ static final int BLINK = 500;
+ private static final float[] TEMP_POSITION = new float[2];
+ private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
+
+ // Cursor Controllers.
+ InsertionPointCursorController mInsertionPointCursorController;
+ SelectionModifierCursorController mSelectionModifierCursorController;
+ ActionMode mSelectionActionMode;
+ boolean mInsertionControllerEnabled;
+ boolean mSelectionControllerEnabled;
+
+ // Used to highlight a word when it is corrected by the IME
+ CorrectionHighlighter mCorrectionHighlighter;
+
+ InputContentType mInputContentType;
+ InputMethodState mInputMethodState;
+
+ DisplayList[] mTextDisplayLists;
+
+ boolean mFrozenWithFocus;
+ boolean mSelectionMoved;
+ boolean mTouchFocusSelected;
+
+ KeyListener mKeyListener;
+ int mInputType = EditorInfo.TYPE_NULL;
+
+ boolean mDiscardNextActionUp;
+ boolean mIgnoreActionUpEvent;
+
+ long mShowCursor;
+ Blink mBlink;
+
+ boolean mCursorVisible = true;
+ boolean mSelectAllOnFocus;
+ boolean mTextIsSelectable;
+
+ CharSequence mError;
+ boolean mErrorWasChanged;
+ ErrorPopup mErrorPopup;
+ /**
+ * This flag is set if the TextView tries to display an error before it
+ * is attached to the window (so its position is still unknown).
+ * It causes the error to be shown later, when onAttachedToWindow()
+ * is called.
+ */
+ boolean mShowErrorAfterAttach;
+
+ boolean mInBatchEditControllers;
+
+ SuggestionsPopupWindow mSuggestionsPopupWindow;
+ SuggestionRangeSpan mSuggestionRangeSpan;
+ Runnable mShowSuggestionRunnable;
+
+ final Drawable[] mCursorDrawable = new Drawable[2];
+ int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
+
+ private Drawable mSelectHandleLeft;
+ private Drawable mSelectHandleRight;
+ private Drawable mSelectHandleCenter;
+
+ // Global listener that detects changes in the global position of the TextView
+ private PositionListener mPositionListener;
+
+ float mLastDownPositionX, mLastDownPositionY;
+ Callback mCustomSelectionActionModeCallback;
+
+ // Set when this TextView gained focus with some text selected. Will start selection mode.
+ boolean mCreatedWithASelection;
+
+ private EasyEditSpanController mEasyEditSpanController;
+
+ WordIterator mWordIterator;
+ SpellChecker mSpellChecker;
+
+ private Rect mTempRect;
+
+ private TextView mTextView;
+
+ Editor(TextView textView) {
+ mTextView = textView;
+ mEasyEditSpanController = new EasyEditSpanController();
+ mTextView.addTextChangedListener(mEasyEditSpanController);
+ }
+
+ void onAttachedToWindow() {
+ if (mShowErrorAfterAttach) {
+ showError();
+ mShowErrorAfterAttach = false;
+ }
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ // No need to create the controller.
+ // The get method will add the listener on controller creation.
+ if (mInsertionPointCursorController != null) {
+ observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+ }
+ if (mSelectionModifierCursorController != null) {
+ observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+ }
+ updateSpellCheckSpans(0, mTextView.getText().length(),
+ true /* create the spell checker if needed */);
+ }
+
+ void onDetachedFromWindow() {
+ if (mError != null) {
+ hideError();
+ }
+
+ if (mBlink != null) {
+ mBlink.removeCallbacks(mBlink);
+ }
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onDetached();
+ }
+
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onDetached();
+ }
+
+ if (mShowSuggestionRunnable != null) {
+ mTextView.removeCallbacks(mShowSuggestionRunnable);
+ }
+
+ invalidateTextDisplayList();
+
+ if (mSpellChecker != null) {
+ mSpellChecker.closeSession();
+ // Forces the creation of a new SpellChecker next time this window is created.
+ // Will handle the cases where the settings has been changed in the meantime.
+ mSpellChecker = null;
+ }
+
+ hideControllers();
+ }
+
+ private void showError() {
+ if (mTextView.getWindowToken() == null) {
+ mShowErrorAfterAttach = true;
+ return;
+ }
+
+ if (mErrorPopup == null) {
+ LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
+ final TextView err = (TextView) inflater.inflate(
+ com.android.internal.R.layout.textview_hint, null);
+
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+ mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
+ mErrorPopup.setFocusable(false);
+ // The user is entering text, so the input method is needed. We
+ // don't want the popup to be displayed on top of it.
+ mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+ }
+
+ TextView tv = (TextView) mErrorPopup.getContentView();
+ chooseSize(mErrorPopup, mError, tv);
+ tv.setText(mError);
+
+ mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
+ mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
+ }
+
+ public void setError(CharSequence error, Drawable icon) {
+ mError = TextUtils.stringOrSpannedString(error);
+ mErrorWasChanged = true;
+ final Drawables dr = mTextView.mDrawables;
+ if (dr != null) {
+ switch (mTextView.getResolvedLayoutDirection()) {
+ default:
+ case View.LAYOUT_DIRECTION_LTR:
+ mTextView.setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
+ dr.mDrawableBottom);
+ break;
+ case View.LAYOUT_DIRECTION_RTL:
+ mTextView.setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
+ dr.mDrawableBottom);
+ break;
+ }
+ } else {
+ mTextView.setCompoundDrawables(null, null, icon, null);
+ }
+
+ if (mError == null) {
+ if (mErrorPopup != null) {
+ if (mErrorPopup.isShowing()) {
+ mErrorPopup.dismiss();
+ }
+
+ mErrorPopup = null;
+ }
+ } else {
+ if (mTextView.isFocused()) {
+ showError();
+ }
+ }
+ }
+
+ private void hideError() {
+ if (mErrorPopup != null) {
+ if (mErrorPopup.isShowing()) {
+ mErrorPopup.dismiss();
+ }
+ }
+
+ mShowErrorAfterAttach = false;
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the middle of the error icon.
+ */
+ private int getErrorX() {
+ /*
+ * The "25" is the distance between the point and the right edge
+ * of the background
+ */
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+
+ final Drawables dr = mTextView.mDrawables;
+ return mTextView.getWidth() - mErrorPopup.getWidth() - mTextView.getPaddingRight() -
+ (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the bottom of the error icon.
+ */
+ private int getErrorY() {
+ /*
+ * Compound, not extended, because the icon is not clipped
+ * if the text height is smaller.
+ */
+ final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
+ int vspace = mTextView.getBottom() - mTextView.getTop() -
+ mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
+
+ final Drawables dr = mTextView.mDrawables;
+ int icontop = compoundPaddingTop +
+ (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
+
+ /*
+ * The "2" is the distance between the point and the top edge
+ * of the background.
+ */
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+ return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - mTextView.getHeight() -
+ (int) (2 * scale + 0.5f);
+ }
+
+ void createInputContentTypeIfNeeded() {
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ }
+
+ void createInputMethodStateIfNeeded() {
+ if (mInputMethodState == null) {
+ mInputMethodState = new InputMethodState();
+ }
+ }
+
+ boolean isCursorVisible() {
+ // The default value is true, even when there is no associated Editor
+ return mCursorVisible && mTextView.isTextEditable();
+ }
+
+ void prepareCursorControllers() {
+ boolean windowSupportsHandles = false;
+
+ ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
+ if (params instanceof WindowManager.LayoutParams) {
+ WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
+ windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
+ || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
+ }
+
+ boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
+ mInsertionControllerEnabled = enabled && isCursorVisible();
+ mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
+
+ if (!mInsertionControllerEnabled) {
+ hideInsertionPointCursorController();
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onDetached();
+ mInsertionPointCursorController = null;
+ }
+ }
+
+ if (!mSelectionControllerEnabled) {
+ stopSelectionActionMode();
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onDetached();
+ mSelectionModifierCursorController = null;
+ }
+ }
+ }
+
+ private void hideInsertionPointCursorController() {
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.hide();
+ }
+ }
+
+ /**
+ * Hides the insertion controller and stops text selection mode, hiding the selection controller
+ */
+ void hideControllers() {
+ hideCursorControllers();
+ hideSpanControllers();
+ }
+
+ private void hideSpanControllers() {
+ if (mEasyEditSpanController != null) {
+ mEasyEditSpanController.hide();
+ }
+ }
+
+ private void hideCursorControllers() {
+ if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
+ // Should be done before hide insertion point controller since it triggers a show of it
+ mSuggestionsPopupWindow.hide();
+ }
+ hideInsertionPointCursorController();
+ stopSelectionActionMode();
+ }
+
+ /**
+ * Create new SpellCheckSpans on the modified region.
+ */
+ private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
+ if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
+ !(mTextView instanceof ExtractEditText)) {
+ if (mSpellChecker == null && createSpellChecker) {
+ mSpellChecker = new SpellChecker(mTextView);
+ }
+ if (mSpellChecker != null) {
+ mSpellChecker.spellCheck(start, end);
+ }
+ }
+ }
+
+ void onScreenStateChanged(int screenState) {
+ switch (screenState) {
+ case View.SCREEN_STATE_ON:
+ resumeBlink();
+ break;
+ case View.SCREEN_STATE_OFF:
+ suspendBlink();
+ break;
+ }
+ }
+
+ private void suspendBlink() {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ }
+
+ private void resumeBlink() {
+ if (mBlink != null) {
+ mBlink.uncancel();
+ makeBlink();
+ }
+ }
+
+ void adjustInputType(boolean password, boolean passwordInputType,
+ boolean webPasswordInputType, boolean numberPasswordInputType) {
+ // mInputType has been set from inputType, possibly modified by mInputMethod.
+ // Specialize mInputType to [web]password if we have a text class and the original input
+ // type was a password.
+ if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+ if (password || passwordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+ if (webPasswordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
+ }
+ } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
+ if (numberPasswordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
+ }
+ }
+ }
+
+ private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
+ int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+ int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+ int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.textview_error_popup_default_width);
+ Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
+ Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
+ float max = 0;
+ for (int i = 0; i < l.getLineCount(); i++) {
+ max = Math.max(max, l.getLineWidth(i));
+ }
+
+ /*
+ * Now set the popup size to be big enough for the text plus the border capped
+ * to DEFAULT_MAX_POPUP_WIDTH
+ */
+ pop.setWidth(wid + (int) Math.ceil(max));
+ pop.setHeight(ht + l.getHeight());
+ }
+
+ void setFrame() {
+ if (mErrorPopup != null) {
+ TextView tv = (TextView) mErrorPopup.getContentView();
+ chooseSize(mErrorPopup, mError, tv);
+ mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
+ mErrorPopup.getWidth(), mErrorPopup.getHeight());
+ }
+ }
+
+ /**
+ * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state
+ * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have
+ * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
+ */
+ private boolean canSelectText() {
+ return hasSelectionController() && mTextView.getText().length() != 0;
+ }
+
+ /**
+ * It would be better to rely on the input type for everything. A password inputType should have
+ * a password transformation. We should hence use isPasswordInputType instead of this method.
+ *
+ * We should:
+ * - Call setInputType in setKeyListener instead of changing the input type directly (which
+ * would install the correct transformation).
+ * - Refuse the installation of a non-password transformation in setTransformation if the input
+ * type is password.
+ *
+ * However, this is like this for legacy reasons and we cannot break existing apps. This method
+ * is useful since it matches what the user can see (obfuscated text or not).
+ *
+ * @return true if the current transformation method is of the password type.
+ */
+ private boolean hasPasswordTransformationMethod() {
+ return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
+ }
+
+ /**
+ * Adjusts selection to the word under last touch offset.
+ * Return true if the operation was successfully performed.
+ */
+ private boolean selectCurrentWord() {
+ if (!canSelectText()) {
+ return false;
+ }
+
+ if (hasPasswordTransformationMethod()) {
+ // Always select all on a password field.
+ // Cut/copy menu entries are not available for passwords, but being able to select all
+ // is however useful to delete or paste to replace the entire content.
+ return mTextView.selectAllText();
+ }
+
+ int inputType = mTextView.getInputType();
+ int klass = inputType & InputType.TYPE_MASK_CLASS;
+ int variation = inputType & InputType.TYPE_MASK_VARIATION;
+
+ // Specific text field types: select the entire text for these
+ if (klass == InputType.TYPE_CLASS_NUMBER ||
+ klass == InputType.TYPE_CLASS_PHONE ||
+ klass == InputType.TYPE_CLASS_DATETIME ||
+ variation == InputType.TYPE_TEXT_VARIATION_URI ||
+ variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
+ variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
+ variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+ return mTextView.selectAllText();
+ }
+
+ long lastTouchOffsets = getLastTouchOffsets();
+ final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
+ final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
+
+ // Safety check in case standard touch event handling has been bypassed
+ if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
+ if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
+
+ int selectionStart, selectionEnd;
+
+ // If a URLSpan (web address, email, phone...) is found at that position, select it.
+ URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
+ getSpans(minOffset, maxOffset, URLSpan.class);
+ if (urlSpans.length >= 1) {
+ URLSpan urlSpan = urlSpans[0];
+ selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
+ selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
+ } else {
+ final WordIterator wordIterator = getWordIterator();
+ wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
+
+ selectionStart = wordIterator.getBeginning(minOffset);
+ selectionEnd = wordIterator.getEnd(maxOffset);
+
+ if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
+ selectionStart == selectionEnd) {
+ // Possible when the word iterator does not properly handle the text's language
+ long range = getCharRange(minOffset);
+ selectionStart = TextUtils.unpackRangeStartFromLong(range);
+ selectionEnd = TextUtils.unpackRangeEndFromLong(range);
+ }
+ }
+
+ Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+ return selectionEnd > selectionStart;
+ }
+
+ void onLocaleChanged() {
+ // Will be re-created on demand in getWordIterator with the proper new locale
+ mWordIterator = null;
+ }
+
+ /**
+ * @hide
+ */
+ public WordIterator getWordIterator() {
+ if (mWordIterator == null) {
+ mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
+ }
+ return mWordIterator;
+ }
+
+ private long getCharRange(int offset) {
+ final int textLength = mTextView.getText().length();
+ if (offset + 1 < textLength) {
+ final char currentChar = mTextView.getText().charAt(offset);
+ final char nextChar = mTextView.getText().charAt(offset + 1);
+ if (Character.isSurrogatePair(currentChar, nextChar)) {
+ return TextUtils.packRangeInLong(offset, offset + 2);
+ }
+ }
+ if (offset < textLength) {
+ return TextUtils.packRangeInLong(offset, offset + 1);
+ }
+ if (offset - 2 >= 0) {
+ final char previousChar = mTextView.getText().charAt(offset - 1);
+ final char previousPreviousChar = mTextView.getText().charAt(offset - 2);
+ if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
+ return TextUtils.packRangeInLong(offset - 2, offset);
+ }
+ }
+ if (offset - 1 >= 0) {
+ return TextUtils.packRangeInLong(offset - 1, offset);
+ }
+ return TextUtils.packRangeInLong(offset, offset);
+ }
+
+ private boolean touchPositionIsInSelection() {
+ int selectionStart = mTextView.getSelectionStart();
+ int selectionEnd = mTextView.getSelectionEnd();
+
+ if (selectionStart == selectionEnd) {
+ return false;
+ }
+
+ if (selectionStart > selectionEnd) {
+ int tmp = selectionStart;
+ selectionStart = selectionEnd;
+ selectionEnd = tmp;
+ Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+ }
+
+ SelectionModifierCursorController selectionController = getSelectionController();
+ int minOffset = selectionController.getMinTouchOffset();
+ int maxOffset = selectionController.getMaxTouchOffset();
+
+ return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
+ }
+
+ private PositionListener getPositionListener() {
+ if (mPositionListener == null) {
+ mPositionListener = new PositionListener();
+ }
+ return mPositionListener;
+ }
+
+ private interface TextViewPositionListener {
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled);
+ }
+
+ private boolean isPositionVisible(int positionX, int positionY) {
+ synchronized (TEMP_POSITION) {
+ final float[] position = TEMP_POSITION;
+ position[0] = positionX;
+ position[1] = positionY;
+ View view = mTextView;
+
+ while (view != null) {
+ if (view != mTextView) {
+ // Local scroll is already taken into account in positionX/Y
+ position[0] -= view.getScrollX();
+ position[1] -= view.getScrollY();
+ }
+
+ if (position[0] < 0 || position[1] < 0 ||
+ position[0] > view.getWidth() || position[1] > view.getHeight()) {
+ return false;
+ }
+
+ if (!view.getMatrix().isIdentity()) {
+ view.getMatrix().mapPoints(position);
+ }
+
+ position[0] += view.getLeft();
+ position[1] += view.getTop();
+
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ view = (View) parent;
+ } else {
+ // We've reached the ViewRoot, stop iterating
+ view = null;
+ }
+ }
+ }
+
+ // We've been able to walk up the view hierarchy and the position was never clipped
+ return true;
+ }
+
+ private boolean isOffsetVisible(int offset) {
+ Layout layout = mTextView.getLayout();
+ final int line = layout.getLineForOffset(offset);
+ final int lineBottom = layout.getLineBottom(line);
+ final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
+ return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
+ lineBottom + mTextView.viewportToContentVerticalOffset());
+ }
+
+ /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
+ * in the view. Returns false when the position is in the empty space of left/right of text.
+ */
+ private boolean isPositionOnText(float x, float y) {
+ Layout layout = mTextView.getLayout();
+ if (layout == null) return false;
+
+ final int line = mTextView.getLineAtCoordinate(y);
+ x = mTextView.convertToLocalHorizontalCoordinate(x);
+
+ if (x < layout.getLineLeft(line)) return false;
+ if (x > layout.getLineRight(line)) return false;
+ return true;
+ }
+
+ public boolean performLongClick(boolean handled) {
+ // Long press in empty space moves cursor and shows the Paste affordance if available.
+ if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
+ mInsertionControllerEnabled) {
+ final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
+ mLastDownPositionY);
+ stopSelectionActionMode();
+ Selection.setSelection((Spannable) mTextView.getText(), offset);
+ getInsertionController().showWithActionPopup();
+ handled = true;
+ }
+
+ if (!handled && mSelectionActionMode != null) {
+ if (touchPositionIsInSelection()) {
+ // Start a drag
+ final int start = mTextView.getSelectionStart();
+ final int end = mTextView.getSelectionEnd();
+ CharSequence selectedText = mTextView.getTransformedText(start, end);
+ ClipData data = ClipData.newPlainText(null, selectedText);
+ DragLocalState localState = new DragLocalState(mTextView, start, end);
+ mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
+ stopSelectionActionMode();
+ } else {
+ getSelectionController().hide();
+ selectCurrentWord();
+ getSelectionController().show();
+ }
+ handled = true;
+ }
+
+ // Start a new selection
+ if (!handled) {
+ handled = startSelectionActionMode();
+ }
+
+ return handled;
+ }
+
+ private long getLastTouchOffsets() {
+ SelectionModifierCursorController selectionController = getSelectionController();
+ final int minOffset = selectionController.getMinTouchOffset();
+ final int maxOffset = selectionController.getMaxTouchOffset();
+ return TextUtils.packRangeInLong(minOffset, maxOffset);
+ }
+
+ void onFocusChanged(boolean focused, int direction) {
+ mShowCursor = SystemClock.uptimeMillis();
+ ensureEndedBatchEdit();
+
+ if (focused) {
+ int selStart = mTextView.getSelectionStart();
+ int selEnd = mTextView.getSelectionEnd();
+
+ // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
+ // mode for these, unless there was a specific selection already started.
+ final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
+ selEnd == mTextView.getText().length();
+
+ mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
+ !isFocusHighlighted;
+
+ if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
+ // If a tap was used to give focus to that view, move cursor at tap position.
+ // Has to be done before onTakeFocus, which can be overloaded.
+ final int lastTapPosition = getLastTapPosition();
+ if (lastTapPosition >= 0) {
+ Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
+ }
+
+ // Note this may have to be moved out of the Editor class
+ MovementMethod mMovement = mTextView.getMovementMethod();
+ if (mMovement != null) {
+ mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
+ }
+
+ // The DecorView does not have focus when the 'Done' ExtractEditText button is
+ // pressed. Since it is the ViewAncestor's mView, it requests focus before
+ // ExtractEditText clears focus, which gives focus to the ExtractEditText.
+ // This special case ensure that we keep current selection in that case.
+ // It would be better to know why the DecorView does not have focus at that time.
+ if (((mTextView instanceof ExtractEditText) || mSelectionMoved) &&
+ selStart >= 0 && selEnd >= 0) {
+ /*
+ * Someone intentionally set the selection, so let them
+ * do whatever it is that they wanted to do instead of
+ * the default on-focus behavior. We reset the selection
+ * here instead of just skipping the onTakeFocus() call
+ * because some movement methods do something other than
+ * just setting the selection in theirs and we still
+ * need to go through that path.
+ */
+ Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
+ }
+
+ if (mSelectAllOnFocus) {
+ mTextView.selectAllText();
+ }
+
+ mTouchFocusSelected = true;
+ }
+
+ mFrozenWithFocus = false;
+ mSelectionMoved = false;
+
+ if (mError != null) {
+ showError();
+ }
+
+ makeBlink();
+ } else {
+ if (mError != null) {
+ hideError();
+ }
+ // Don't leave us in the middle of a batch edit.
+ mTextView.onEndBatchEdit();
+
+ if (mTextView instanceof ExtractEditText) {
+ // terminateTextSelectionMode removes selection, which we want to keep when
+ // ExtractEditText goes out of focus.
+ final int selStart = mTextView.getSelectionStart();
+ final int selEnd = mTextView.getSelectionEnd();
+ hideControllers();
+ Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
+ } else {
+ hideControllers();
+ downgradeEasyCorrectionSpans();
+ }
+
+ // No need to create the controller
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.resetTouchOffsets();
+ }
+ }
+ }
+
+ /**
+ * Downgrades to simple suggestions all the easy correction spans that are not a spell check
+ * span.
+ */
+ private void downgradeEasyCorrectionSpans() {
+ CharSequence text = mTextView.getText();
+ if (text instanceof Spannable) {
+ Spannable spannable = (Spannable) text;
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
+ spannable.length(), SuggestionSpan.class);
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ int flags = suggestionSpans[i].getFlags();
+ if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
+ && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
+ flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
+ suggestionSpans[i].setFlags(flags);
+ }
+ }
+ }
+ }
+
+ void sendOnTextChanged(int start, int after) {
+ updateSpellCheckSpans(start, start + after, false);
+
+ // Hide the controllers as soon as text is modified (typing, procedural...)
+ // We do not hide the span controllers, since they can be added when a new text is
+ // inserted into the text view (voice IME).
+ hideCursorControllers();
+ }
+
+ private int getLastTapPosition() {
+ // No need to create the controller at that point, no last tap position saved
+ if (mSelectionModifierCursorController != null) {
+ int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
+ if (lastTapPosition >= 0) {
+ // Safety check, should not be possible.
+ if (lastTapPosition > mTextView.getText().length()) {
+ lastTapPosition = mTextView.getText().length();
+ }
+ return lastTapPosition;
+ }
+ }
+
+ return -1;
+ }
+
+ void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (hasWindowFocus) {
+ if (mBlink != null) {
+ mBlink.uncancel();
+ makeBlink();
+ }
+ } else {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ if (mInputContentType != null) {
+ mInputContentType.enterDown = false;
+ }
+ // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
+ hideControllers();
+ if (mSuggestionsPopupWindow != null) {
+ mSuggestionsPopupWindow.onParentLostFocus();
+ }
+
+ // Don't leave us in the middle of a batch edit.
+ mTextView.onEndBatchEdit();
+ }
+ }
+
+ void onTouchEvent(MotionEvent event) {
+ if (hasSelectionController()) {
+ getSelectionController().onTouchEvent(event);
+ }
+
+ if (mShowSuggestionRunnable != null) {
+ mTextView.removeCallbacks(mShowSuggestionRunnable);
+ mShowSuggestionRunnable = null;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mLastDownPositionX = event.getX();
+ mLastDownPositionY = event.getY();
+
+ // Reset this state; it will be re-set if super.onTouchEvent
+ // causes focus to move to the view.
+ mTouchFocusSelected = false;
+ mIgnoreActionUpEvent = false;
+ }
+ }
+
+ public void beginBatchEdit() {
+ mInBatchEditControllers = true;
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = ++ims.mBatchEditNesting;
+ if (nesting == 1) {
+ ims.mCursorChanged = false;
+ ims.mChangedDelta = 0;
+ if (ims.mContentChanged) {
+ // We already have a pending change from somewhere else,
+ // so turn this into a full update.
+ ims.mChangedStart = 0;
+ ims.mChangedEnd = mTextView.getText().length();
+ } else {
+ ims.mChangedStart = EXTRACT_UNKNOWN;
+ ims.mChangedEnd = EXTRACT_UNKNOWN;
+ ims.mContentChanged = false;
+ }
+ mTextView.onBeginBatchEdit();
+ }
+ }
+ }
+
+ public void endBatchEdit() {
+ mInBatchEditControllers = false;
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = --ims.mBatchEditNesting;
+ if (nesting == 0) {
+ finishBatchEdit(ims);
+ }
+ }
+ }
+
+ void ensureEndedBatchEdit() {
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting != 0) {
+ ims.mBatchEditNesting = 0;
+ finishBatchEdit(ims);
+ }
+ }
+
+ void finishBatchEdit(final InputMethodState ims) {
+ mTextView.onEndBatchEdit();
+
+ if (ims.mContentChanged || ims.mSelectionModeChanged) {
+ mTextView.updateAfterEdit();
+ reportExtractedText();
+ } else if (ims.mCursorChanged) {
+ // Cheezy way to get us to report the current cursor location.
+ mTextView.invalidateCursor();
+ }
+ }
+
+ static final int EXTRACT_NOTHING = -2;
+ static final int EXTRACT_UNKNOWN = -1;
+
+ boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+ return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
+ EXTRACT_UNKNOWN, outText);
+ }
+
+ private boolean extractTextInternal(ExtractedTextRequest request,
+ int partialStartOffset, int partialEndOffset, int delta,
+ ExtractedText outText) {
+ final CharSequence content = mTextView.getText();
+ if (content != null) {
+ if (partialStartOffset != EXTRACT_NOTHING) {
+ final int N = content.length();
+ if (partialStartOffset < 0) {
+ outText.partialStartOffset = outText.partialEndOffset = -1;
+ partialStartOffset = 0;
+ partialEndOffset = N;
+ } else {
+ // Now use the delta to determine the actual amount of text
+ // we need.
+ partialEndOffset += delta;
+ // Adjust offsets to ensure we contain full spans.
+ if (content instanceof Spanned) {
+ Spanned spanned = (Spanned)content;
+ Object[] spans = spanned.getSpans(partialStartOffset,
+ partialEndOffset, ParcelableSpan.class);
+ int i = spans.length;
+ while (i > 0) {
+ i--;
+ int j = spanned.getSpanStart(spans[i]);
+ if (j < partialStartOffset) partialStartOffset = j;
+ j = spanned.getSpanEnd(spans[i]);
+ if (j > partialEndOffset) partialEndOffset = j;
+ }
+ }
+ outText.partialStartOffset = partialStartOffset;
+ outText.partialEndOffset = partialEndOffset - delta;
+
+ if (partialStartOffset > N) {
+ partialStartOffset = N;
+ } else if (partialStartOffset < 0) {
+ partialStartOffset = 0;
+ }
+ if (partialEndOffset > N) {
+ partialEndOffset = N;
+ } else if (partialEndOffset < 0) {
+ partialEndOffset = 0;
+ }
+ }
+ if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
+ outText.text = content.subSequence(partialStartOffset,
+ partialEndOffset);
+ } else {
+ outText.text = TextUtils.substring(content, partialStartOffset,
+ partialEndOffset);
+ }
+ } else {
+ outText.partialStartOffset = 0;
+ outText.partialEndOffset = 0;
+ outText.text = "";
+ }
+ outText.flags = 0;
+ if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
+ outText.flags |= ExtractedText.FLAG_SELECTING;
+ }
+ if (mTextView.isSingleLine()) {
+ outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
+ }
+ outText.startOffset = 0;
+ outText.selectionStart = mTextView.getSelectionStart();
+ outText.selectionEnd = mTextView.getSelectionEnd();
+ return true;
+ }
+ return false;
+ }
+
+ boolean reportExtractedText() {
+ final Editor.InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ final boolean contentChanged = ims.mContentChanged;
+ if (contentChanged || ims.mSelectionModeChanged) {
+ ims.mContentChanged = false;
+ ims.mSelectionModeChanged = false;
+ final ExtractedTextRequest req = ims.mExtracting;
+ if (req != null) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
+ "Retrieving extracted start=" + ims.mChangedStart +
+ " end=" + ims.mChangedEnd +
+ " delta=" + ims.mChangedDelta);
+ if (ims.mChangedStart < 0 && !contentChanged) {
+ ims.mChangedStart = EXTRACT_NOTHING;
+ }
+ if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
+ ims.mChangedDelta, ims.mTmpExtracted)) {
+ if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
+ "Reporting extracted start=" +
+ ims.mTmpExtracted.partialStartOffset +
+ " end=" + ims.mTmpExtracted.partialEndOffset +
+ ": " + ims.mTmpExtracted.text);
+ imm.updateExtractedText(mTextView, req.token, ims.mTmpExtracted);
+ ims.mChangedStart = EXTRACT_UNKNOWN;
+ ims.mChangedEnd = EXTRACT_UNKNOWN;
+ ims.mChangedDelta = 0;
+ ims.mContentChanged = false;
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
+ int cursorOffsetVertical) {
+ final int selectionStart = mTextView.getSelectionStart();
+ final int selectionEnd = mTextView.getSelectionEnd();
+
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting == 0) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (imm.isActive(mTextView)) {
+ boolean reported = false;
+ if (ims.mContentChanged || ims.mSelectionModeChanged) {
+ // We are in extract mode and the content has changed
+ // in some way... just report complete new text to the
+ // input method.
+ reported = reportExtractedText();
+ }
+ if (!reported && highlight != null) {
+ int candStart = -1;
+ int candEnd = -1;
+ if (mTextView.getText() instanceof Spannable) {
+ Spannable sp = (Spannable) mTextView.getText();
+ candStart = EditableInputConnection.getComposingSpanStart(sp);
+ candEnd = EditableInputConnection.getComposingSpanEnd(sp);
+ }
+ imm.updateSelection(mTextView,
+ selectionStart, selectionEnd, candStart, candEnd);
+ }
+ }
+
+ if (imm.isWatchingCursor(mTextView) && highlight != null) {
+ highlight.computeBounds(ims.mTmpRectF, true);
+ ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
+
+ canvas.getMatrix().mapPoints(ims.mTmpOffset);
+ ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
+
+ ims.mTmpRectF.offset(0, cursorOffsetVertical);
+
+ ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
+ (int)(ims.mTmpRectF.top + 0.5),
+ (int)(ims.mTmpRectF.right + 0.5),
+ (int)(ims.mTmpRectF.bottom + 0.5));
+
+ imm.updateCursor(mTextView,
+ ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
+ ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
+ }
+ }
+ }
+
+ if (mCorrectionHighlighter != null) {
+ mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
+ }
+
+ if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
+ drawCursor(canvas, cursorOffsetVertical);
+ // Rely on the drawable entirely, do not draw the cursor line.
+ // Has to be done after the IMM related code above which relies on the highlight.
+ highlight = null;
+ }
+
+ if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
+ drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
+ cursorOffsetVertical);
+ } else {
+ layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
+ }
+ }
+
+ private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
+ Paint highlightPaint, int cursorOffsetVertical) {
+ final int width = mTextView.getWidth();
+ final int height = mTextView.getHeight();
+
+ final long lineRange = layout.getLineRangeForDraw(canvas);
+ int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
+ int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
+ if (lastLine < 0) return;
+
+ layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
+ firstLine, lastLine);
+
+ if (layout instanceof DynamicLayout) {
+ if (mTextDisplayLists == null) {
+ mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
+ }
+
+ DynamicLayout dynamicLayout = (DynamicLayout) layout;
+ int[] blockEnds = dynamicLayout.getBlockEnds();
+ int[] blockIndices = dynamicLayout.getBlockIndices();
+ final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
+
+ final int mScrollX = mTextView.getScrollX();
+ final int mScrollY = mTextView.getScrollY();
+ canvas.translate(mScrollX, mScrollY);
+ int endOfPreviousBlock = -1;
+ int searchStartIndex = 0;
+ for (int i = 0; i < numberOfBlocks; i++) {
+ int blockEnd = blockEnds[i];
+ int blockIndex = blockIndices[i];
+
+ final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
+ if (blockIsInvalid) {
+ blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
+ searchStartIndex);
+ // Dynamic layout internal block indices structure is updated from Editor
+ blockIndices[i] = blockIndex;
+ searchStartIndex = blockIndex + 1;
+ }
+
+ DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
+ if (blockDisplayList == null) {
+ blockDisplayList = mTextDisplayLists[blockIndex] =
+ mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex);
+ } else {
+ if (blockIsInvalid) blockDisplayList.invalidate();
+ }
+
+ if (!blockDisplayList.isValid()) {
+ final HardwareCanvas hardwareCanvas = blockDisplayList.start();
+ try {
+ hardwareCanvas.setViewport(width, height);
+ // The dirty rect should always be null for a display list
+ hardwareCanvas.onPreDraw(null);
+ hardwareCanvas.translate(-mScrollX, -mScrollY);
+ layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd);
+ hardwareCanvas.translate(mScrollX, mScrollY);
+ } finally {
+ hardwareCanvas.onPostDraw();
+ blockDisplayList.end();
+ if (View.USE_DISPLAY_LIST_PROPERTIES) {
+ blockDisplayList.setLeftTopRightBottom(0, 0, width, height);
+ }
+ }
+ }
+
+ ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null,
+ DisplayList.FLAG_CLIP_CHILDREN);
+ endOfPreviousBlock = blockEnd;
+ }
+ canvas.translate(-mScrollX, -mScrollY);
+ } else {
+ // Boring layout is used for empty and hint text
+ layout.drawText(canvas, firstLine, lastLine);
+ }
+ }
+
+ private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
+ int searchStartIndex) {
+ int length = mTextDisplayLists.length;
+ for (int i = searchStartIndex; i < length; i++) {
+ boolean blockIndexFound = false;
+ for (int j = 0; j < numberOfBlocks; j++) {
+ if (blockIndices[j] == i) {
+ blockIndexFound = true;
+ break;
+ }
+ }
+ if (blockIndexFound) continue;
+ return i;
+ }
+
+ // No available index found, the pool has to grow
+ int newSize = ArrayUtils.idealIntArraySize(length + 1);
+ DisplayList[] displayLists = new DisplayList[newSize];
+ System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
+ mTextDisplayLists = displayLists;
+ return length;
+ }
+
+ private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
+ final boolean translate = cursorOffsetVertical != 0;
+ if (translate) canvas.translate(0, cursorOffsetVertical);
+ for (int i = 0; i < mCursorCount; i++) {
+ mCursorDrawable[i].draw(canvas);
+ }
+ if (translate) canvas.translate(0, -cursorOffsetVertical);
+ }
+
+ void invalidateTextDisplayList() {
+ if (mTextDisplayLists != null) {
+ for (int i = 0; i < mTextDisplayLists.length; i++) {
+ if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
+ }
+ }
+ }
+
+ void updateCursorsPositions() {
+ if (mTextView.mCursorDrawableRes == 0) {
+ mCursorCount = 0;
+ return;
+ }
+
+ Layout layout = mTextView.getLayout();
+ final int offset = mTextView.getSelectionStart();
+ final int line = layout.getLineForOffset(offset);
+ final int top = layout.getLineTop(line);
+ final int bottom = layout.getLineTop(line + 1);
+
+ mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
+
+ int middle = bottom;
+ if (mCursorCount == 2) {
+ // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
+ middle = (top + bottom) >> 1;
+ }
+
+ updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset));
+
+ if (mCursorCount == 2) {
+ updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset));
+ }
+ }
+
+ /**
+ * @return true if the selection mode was actually started.
+ */
+ boolean startSelectionActionMode() {
+ if (mSelectionActionMode != null) {
+ // Selection action mode is already started
+ return false;
+ }
+
+ if (!canSelectText() || !mTextView.requestFocus()) {
+ Log.w(TextView.LOG_TAG,
+ "TextView does not support text selection. Action mode cancelled.");
+ return false;
+ }
+
+ if (!mTextView.hasSelection()) {
+ // There may already be a selection on device rotation
+ if (!selectCurrentWord()) {
+ // No word found under cursor or text selection not permitted.
+ return false;
+ }
+ }
+
+ boolean willExtract = extractedTextModeWillBeStarted();
+
+ // Do not start the action mode when extracted text will show up full screen, which would
+ // immediately hide the newly created action bar and would be visually distracting.
+ if (!willExtract) {
+ ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
+ mSelectionActionMode = mTextView.startActionMode(actionModeCallback);
+ }
+
+ final boolean selectionStarted = mSelectionActionMode != null || willExtract;
+ if (selectionStarted && !mTextView.isTextSelectable()) {
+ // Show the IME to be able to replace text, except when selecting non editable text.
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.showSoftInput(mTextView, 0, null);
+ }
+ }
+
+ return selectionStarted;
+ }
+
+ private boolean extractedTextModeWillBeStarted() {
+ if (!(mTextView instanceof ExtractEditText)) {
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ return imm != null && imm.isFullscreenMode();
+ }
+ return false;
+ }
+
+ /**
+ * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
+ */
+ private boolean isCursorInsideSuggestionSpan() {
+ CharSequence text = mTextView.getText();
+ if (!(text instanceof Spannable)) return false;
+
+ SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans(
+ mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class);
+ return (suggestionSpans.length > 0);
+ }
+
+ /**
+ * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
+ * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
+ */
+ private boolean isCursorInsideEasyCorrectionSpan() {
+ Spannable spannable = (Spannable) mTextView.getText();
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
+ mTextView.getSelectionEnd(), SuggestionSpan.class);
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void onTouchUpEvent(MotionEvent event) {
+ boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
+ hideControllers();
+ CharSequence text = mTextView.getText();
+ if (!selectAllGotFocus && text.length() > 0) {
+ // Move cursor
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+ Selection.setSelection((Spannable) text, offset);
+ if (mSpellChecker != null) {
+ // When the cursor moves, the word that was typed may need spell check
+ mSpellChecker.onSelectionChanged();
+ }
+ if (!extractedTextModeWillBeStarted()) {
+ if (isCursorInsideEasyCorrectionSpan()) {
+ mShowSuggestionRunnable = new Runnable() {
+ public void run() {
+ showSuggestions();
+ }
+ };
+ // removeCallbacks is performed on every touch
+ mTextView.postDelayed(mShowSuggestionRunnable,
+ ViewConfiguration.getDoubleTapTimeout());
+ } else if (hasInsertionController()) {
+ getInsertionController().show();
+ }
+ }
+ }
+ }
+
+ protected void stopSelectionActionMode() {
+ if (mSelectionActionMode != null) {
+ // This will hide the mSelectionModifierCursorController
+ mSelectionActionMode.finish();
+ }
+ }
+
+ /**
+ * @return True if this view supports insertion handles.
+ */
+ boolean hasInsertionController() {
+ return mInsertionControllerEnabled;
+ }
+
+ /**
+ * @return True if this view supports selection handles.
+ */
+ boolean hasSelectionController() {
+ return mSelectionControllerEnabled;
+ }
+
+ InsertionPointCursorController getInsertionController() {
+ if (!mInsertionControllerEnabled) {
+ return null;
+ }
+
+ if (mInsertionPointCursorController == null) {
+ mInsertionPointCursorController = new InsertionPointCursorController();
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+ }
+
+ return mInsertionPointCursorController;
+ }
+
+ SelectionModifierCursorController getSelectionController() {
+ if (!mSelectionControllerEnabled) {
+ return null;
+ }
+
+ if (mSelectionModifierCursorController == null) {
+ mSelectionModifierCursorController = new SelectionModifierCursorController();
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+ }
+
+ return mSelectionModifierCursorController;
+ }
+
+ private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
+ if (mCursorDrawable[cursorIndex] == null)
+ mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable(
+ mTextView.mCursorDrawableRes);
+
+ if (mTempRect == null) mTempRect = new Rect();
+ mCursorDrawable[cursorIndex].getPadding(mTempRect);
+ final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
+ horizontal = Math.max(0.5f, horizontal - 0.5f);
+ final int left = (int) (horizontal) - mTempRect.left;
+ mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
+ bottom + mTempRect.bottom);
+ }
+
+ /**
+ * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+ * a dictionnary) from the current input method, provided by it calling
+ * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
+ * implementation flashes the background of the corrected word to provide feedback to the user.
+ *
+ * @param info The auto correct info about the text that was corrected.
+ */
+ public void onCommitCorrection(CorrectionInfo info) {
+ if (mCorrectionHighlighter == null) {
+ mCorrectionHighlighter = new CorrectionHighlighter();
+ } else {
+ mCorrectionHighlighter.invalidate(false);
+ }
+
+ mCorrectionHighlighter.highlight(info);
+ }
+
+ void showSuggestions() {
+ if (mSuggestionsPopupWindow == null) {
+ mSuggestionsPopupWindow = new SuggestionsPopupWindow();
+ }
+ hideControllers();
+ mSuggestionsPopupWindow.show();
+ }
+
+ boolean areSuggestionsShown() {
+ return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
+ }
+
+ void onScrollChanged() {
+ if (mPositionListener != null) {
+ mPositionListener.onScrollChanged();
+ }
+ // Internal scroll affects the clip boundaries
+ invalidateTextDisplayList();
+ }
+
+ /**
+ * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
+ */
+ private boolean shouldBlink() {
+ if (!isCursorVisible() || !mTextView.isFocused()) return false;
+
+ final int start = mTextView.getSelectionStart();
+ if (start < 0) return false;
+
+ final int end = mTextView.getSelectionEnd();
+ if (end < 0) return false;
+
+ return start == end;
+ }
+
+ void makeBlink() {
+ if (shouldBlink()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ if (mBlink == null) mBlink = new Blink();
+ mBlink.removeCallbacks(mBlink);
+ mBlink.postAtTime(mBlink, mShowCursor + BLINK);
+ } else {
+ if (mBlink != null) mBlink.removeCallbacks(mBlink);
+ }
+ }
+
+ private class Blink extends Handler implements Runnable {
+ private boolean mCancelled;
+
+ public void run() {
+ Log.d("GILLES", "blinking !!!");
+ if (mCancelled) {
+ return;
+ }
+
+ removeCallbacks(Blink.this);
+
+ if (shouldBlink()) {
+ if (mTextView.getLayout() != null) {
+ mTextView.invalidateCursorPath();
+ }
+
+ postAtTime(this, SystemClock.uptimeMillis() + BLINK);
+ }
+ }
+
+ void cancel() {
+ if (!mCancelled) {
+ removeCallbacks(Blink.this);
+ mCancelled = true;
+ }
+ }
+
+ void uncancel() {
+ mCancelled = false;
+ }
+ }
+
+ private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
+ TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
+ com.android.internal.R.layout.text_drag_thumbnail, null);
+
+ if (shadowView == null) {
+ throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
+ }
+
+ if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
+ text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
+ }
+ shadowView.setText(text);
+ shadowView.setTextColor(mTextView.getTextColors());
+
+ shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge);
+ shadowView.setGravity(Gravity.CENTER);
+
+ shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ shadowView.measure(size, size);
+
+ shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
+ shadowView.invalidate();
+ return new DragShadowBuilder(shadowView);
+ }
+
+ private static class DragLocalState {
+ public TextView sourceTextView;
+ public int start, end;
+
+ public DragLocalState(TextView sourceTextView, int start, int end) {
+ this.sourceTextView = sourceTextView;
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ void onDrop(DragEvent event) {
+ StringBuilder content = new StringBuilder("");
+ ClipData clipData = event.getClipData();
+ final int itemCount = clipData.getItemCount();
+ for (int i=0; i < itemCount; i++) {
+ Item item = clipData.getItemAt(i);
+ content.append(item.coerceToText(mTextView.getContext()));
+ }
+
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+
+ Object localState = event.getLocalState();
+ DragLocalState dragLocalState = null;
+ if (localState instanceof DragLocalState) {
+ dragLocalState = (DragLocalState) localState;
+ }
+ boolean dragDropIntoItself = dragLocalState != null &&
+ dragLocalState.sourceTextView == mTextView;
+
+ if (dragDropIntoItself) {
+ if (offset >= dragLocalState.start && offset < dragLocalState.end) {
+ // A drop inside the original selection discards the drop.
+ return;
+ }
+ }
+
+ final int originalLength = mTextView.getText().length();
+ long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content);
+ int min = TextUtils.unpackRangeStartFromLong(minMax);
+ int max = TextUtils.unpackRangeEndFromLong(minMax);
+
+ Selection.setSelection((Spannable) mTextView.getText(), max);
+ mTextView.replaceText_internal(min, max, content);
+
+ if (dragDropIntoItself) {
+ int dragSourceStart = dragLocalState.start;
+ int dragSourceEnd = dragLocalState.end;
+ if (max <= dragSourceStart) {
+ // Inserting text before selection has shifted positions
+ final int shift = mTextView.getText().length() - originalLength;
+ dragSourceStart += shift;
+ dragSourceEnd += shift;
+ }
+
+ // Delete original selection
+ mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
+
+ // Make sure we do not leave two adjacent spaces.
+ CharSequence t = mTextView.getTransformedText(dragSourceStart - 1, dragSourceStart + 1);
+ if ( (dragSourceStart == 0 || Character.isSpaceChar(t.charAt(0))) &&
+ (dragSourceStart == mTextView.getText().length() ||
+ Character.isSpaceChar(t.charAt(1))) ) {
+ final int pos = dragSourceStart == mTextView.getText().length() ?
+ dragSourceStart - 1 : dragSourceStart;
+ mTextView.deleteText_internal(pos, pos + 1);
+ }
+ }
+ }
+
+ /**
+ * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
+ * pop-up should be displayed.
+ */
+ class EasyEditSpanController implements TextWatcher {
+
+ private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
+
+ private EasyEditPopupWindow mPopupWindow;
+
+ private EasyEditSpan mEasyEditSpan;
+
+ private Runnable mHidePopup;
+
+ public void hide() {
+ if (mPopupWindow != null) {
+ mPopupWindow.hide();
+ mTextView.removeCallbacks(mHidePopup);
+ }
+ removeSpans(mTextView.getText());
+ mEasyEditSpan = null;
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Intentionally empty
+ }
+
+ public void afterTextChanged(Editable s) {
+ // Intentionally empty
+ }
+
+ /**
+ * Monitors the changes in the text.
+ *
+ * <p>{@link SpanWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
+ * as the notifications are not sent when a spannable (with spans) is inserted.
+ */
+ public void onTextChanged(CharSequence buffer, int start, int before, int after) {
+ adjustSpans(buffer, start, after);
+
+ if (mTextView.getWindowVisibility() != View.VISIBLE) {
+ // The window is not visible yet, ignore the text change.
+ return;
+ }
+
+ if (mTextView.getLayout() == null) {
+ // The view has not been layout yet, ignore the text change
+ return;
+ }
+
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (!(mTextView instanceof ExtractEditText) && imm != null && imm.isFullscreenMode()) {
+ // The input is in extract mode. We do not have to handle the easy edit in the
+ // original TextView, as the ExtractEditText will do
+ return;
+ }
+
+ // Remove the current easy edit span, as the text changed, and remove the pop-up
+ // (if any)
+ if (mEasyEditSpan != null) {
+ if (buffer instanceof Spannable) {
+ ((Spannable) buffer).removeSpan(mEasyEditSpan);
+ }
+ mEasyEditSpan = null;
+ }
+ if (mPopupWindow != null && mPopupWindow.isShowing()) {
+ mPopupWindow.hide();
+ }
+
+ // Display the new easy edit span (if any).
+ if (buffer instanceof Spanned) {
+ mEasyEditSpan = getSpan((Spanned) buffer);
+ if (mEasyEditSpan != null) {
+ if (mPopupWindow == null) {
+ mPopupWindow = new EasyEditPopupWindow();
+ mHidePopup = new Runnable() {
+ @Override
+ public void run() {
+ hide();
+ }
+ };
+ }
+ mPopupWindow.show(mEasyEditSpan);
+ mTextView.removeCallbacks(mHidePopup);
+ mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
+ }
+ }
+ }
+
+ /**
+ * Adjusts the spans by removing all of them except the last one.
+ */
+ private void adjustSpans(CharSequence buffer, int start, int after) {
+ // This method enforces that only one easy edit span is attached to the text.
+ // A better way to enforce this would be to listen for onSpanAdded, but this method
+ // cannot be used in this scenario as no notification is triggered when a text with
+ // spans is inserted into a text.
+ if (buffer instanceof Spannable) {
+ Spannable spannable = (Spannable) buffer;
+ EasyEditSpan[] spans = spannable.getSpans(start, start + after, EasyEditSpan.class);
+ if (spans.length > 0) {
+ // Assuming there was only one EasyEditSpan before, we only need check to
+ // check for a duplicate if a new one is found in the modified interval
+ spans = spannable.getSpans(0, spannable.length(), EasyEditSpan.class);
+ for (int i = 1; i < spans.length; i++) {
+ spannable.removeSpan(spans[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes all the {@link EasyEditSpan} currently attached.
+ */
+ private void removeSpans(CharSequence buffer) {
+ if (buffer instanceof Spannable) {
+ Spannable spannable = (Spannable) buffer;
+ EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
+ EasyEditSpan.class);
+ for (int i = 0; i < spans.length; i++) {
+ spannable.removeSpan(spans[i]);
+ }
+ }
+ }
+
+ private EasyEditSpan getSpan(Spanned spanned) {
+ EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
+ EasyEditSpan.class);
+ if (easyEditSpans.length == 0) {
+ return null;
+ } else {
+ return easyEditSpans[0];
+ }
+ }
+ }
+
+ /**
+ * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
+ * by {@link EasyEditSpanController}.
+ */
+ private class EasyEditPopupWindow extends PinnedPopupWindow
+ implements OnClickListener {
+ private static final int POPUP_TEXT_LAYOUT =
+ com.android.internal.R.layout.text_edit_action_popup_text;
+ private TextView mDeleteTextView;
+ private EasyEditSpan mEasyEditSpan;
+
+ @Override
+ protected void createPopupWindow() {
+ mPopupWindow = new PopupWindow(mTextView.getContext(), null,
+ com.android.internal.R.attr.textSelectHandleWindowStyle);
+ mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopupWindow.setClippingEnabled(true);
+ }
+
+ @Override
+ protected void initContentView() {
+ LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
+ linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ mContentView = linearLayout;
+ mContentView.setBackgroundResource(
+ com.android.internal.R.drawable.text_edit_side_paste_window);
+
+ LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LayoutParams wrapContent = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+ mDeleteTextView.setLayoutParams(wrapContent);
+ mDeleteTextView.setText(com.android.internal.R.string.delete);
+ mDeleteTextView.setOnClickListener(this);
+ mContentView.addView(mDeleteTextView);
+ }
+
+ public void show(EasyEditSpan easyEditSpan) {
+ mEasyEditSpan = easyEditSpan;
+ super.show();
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == mDeleteTextView) {
+ Editable editable = (Editable) mTextView.getText();
+ int start = editable.getSpanStart(mEasyEditSpan);
+ int end = editable.getSpanEnd(mEasyEditSpan);
+ if (start >= 0 && end >= 0) {
+ mTextView.deleteText_internal(start, end);
+ }
+ }
+ }
+
+ @Override
+ protected int getTextOffset() {
+ // Place the pop-up at the end of the span
+ Editable editable = (Editable) mTextView.getText();
+ return editable.getSpanEnd(mEasyEditSpan);
+ }
+
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ return mTextView.getLayout().getLineBottom(line);
+ }
+
+ @Override
+ protected int clipVertically(int positionY) {
+ // As we display the pop-up below the span, no vertical clipping is required.
+ return positionY;
+ }
+ }
+
+ private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
+ // 3 handles
+ // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
+ private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
+ private TextViewPositionListener[] mPositionListeners =
+ new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean mPositionHasChanged = true;
+ // Absolute position of the TextView with respect to its parent window
+ private int mPositionX, mPositionY;
+ private int mNumberOfListeners;
+ private boolean mScrollHasChanged;
+ final int[] mTempCoords = new int[2];
+
+ public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
+ if (mNumberOfListeners == 0) {
+ updatePosition();
+ ViewTreeObserver vto = mTextView.getViewTreeObserver();
+ vto.addOnPreDrawListener(this);
+ }
+
+ int emptySlotIndex = -1;
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ TextViewPositionListener listener = mPositionListeners[i];
+ if (listener == positionListener) {
+ return;
+ } else if (emptySlotIndex < 0 && listener == null) {
+ emptySlotIndex = i;
+ }
+ }
+
+ mPositionListeners[emptySlotIndex] = positionListener;
+ mCanMove[emptySlotIndex] = canMove;
+ mNumberOfListeners++;
+ }
+
+ public void removeSubscriber(TextViewPositionListener positionListener) {
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionListeners[i] == positionListener) {
+ mPositionListeners[i] = null;
+ mNumberOfListeners--;
+ break;
+ }
+ }
+
+ if (mNumberOfListeners == 0) {
+ ViewTreeObserver vto = mTextView.getViewTreeObserver();
+ vto.removeOnPreDrawListener(this);
+ }
+ }
+
+ public int getPositionX() {
+ return mPositionX;
+ }
+
+ public int getPositionY() {
+ return mPositionY;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ updatePosition();
+
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
+ TextViewPositionListener positionListener = mPositionListeners[i];
+ if (positionListener != null) {
+ positionListener.updatePosition(mPositionX, mPositionY,
+ mPositionHasChanged, mScrollHasChanged);
+ }
+ }
+ }
+
+ mScrollHasChanged = false;
+ return true;
+ }
+
+ private void updatePosition() {
+ mTextView.getLocationInWindow(mTempCoords);
+
+ mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
+
+ mPositionX = mTempCoords[0];
+ mPositionY = mTempCoords[1];
+ }
+
+ public void onScrollChanged() {
+ mScrollHasChanged = true;
+ }
+ }
+
+ private abstract class PinnedPopupWindow implements TextViewPositionListener {
+ protected PopupWindow mPopupWindow;
+ protected ViewGroup mContentView;
+ int mPositionX, mPositionY;
+
+ protected abstract void createPopupWindow();
+ protected abstract void initContentView();
+ protected abstract int getTextOffset();
+ protected abstract int getVerticalLocalPosition(int line);
+ protected abstract int clipVertically(int positionY);
+
+ public PinnedPopupWindow() {
+ createPopupWindow();
+
+ mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+ mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ initContentView();
+
+ LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ mContentView.setLayoutParams(wrapContent);
+
+ mPopupWindow.setContentView(mContentView);
+ }
+
+ public void show() {
+ getPositionListener().addSubscriber(this, false /* offset is fixed */);
+
+ computeLocalPosition();
+
+ final PositionListener positionListener = getPositionListener();
+ updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
+ }
+
+ protected void measureContent() {
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ mContentView.measure(
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
+ View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
+ View.MeasureSpec.AT_MOST));
+ }
+
+ /* The popup window will be horizontally centered on the getTextOffset() and vertically
+ * positioned according to viewportToContentHorizontalOffset.
+ *
+ * This method assumes that mContentView has properly been measured from its content. */
+ private void computeLocalPosition() {
+ measureContent();
+ final int width = mContentView.getMeasuredWidth();
+ final int offset = getTextOffset();
+ mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
+ mPositionX += mTextView.viewportToContentHorizontalOffset();
+
+ final int line = mTextView.getLayout().getLineForOffset(offset);
+ mPositionY = getVerticalLocalPosition(line);
+ mPositionY += mTextView.viewportToContentVerticalOffset();
+ }
+
+ private void updatePosition(int parentPositionX, int parentPositionY) {
+ int positionX = parentPositionX + mPositionX;
+ int positionY = parentPositionY + mPositionY;
+
+ positionY = clipVertically(positionY);
+
+ // Horizontal clipping
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ final int width = mContentView.getMeasuredWidth();
+ positionX = Math.min(displayMetrics.widthPixels - width, positionX);
+ positionX = Math.max(0, positionX);
+
+ if (isShowing()) {
+ mPopupWindow.update(positionX, positionY, -1, -1);
+ } else {
+ mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+ positionX, positionY);
+ }
+ }
+
+ public void hide() {
+ mPopupWindow.dismiss();
+ getPositionListener().removeSubscriber(this);
+ }
+
+ @Override
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled) {
+ // Either parentPositionChanged or parentScrolled is true, check if still visible
+ if (isShowing() && isOffsetVisible(getTextOffset())) {
+ if (parentScrolled) computeLocalPosition();
+ updatePosition(parentPositionX, parentPositionY);
+ } else {
+ hide();
+ }
+ }
+
+ public boolean isShowing() {
+ return mPopupWindow.isShowing();
+ }
+ }
+
+ private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
+ private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
+ private static final int ADD_TO_DICTIONARY = -1;
+ private static final int DELETE_TEXT = -2;
+ private SuggestionInfo[] mSuggestionInfos;
+ private int mNumberOfSuggestions;
+ private boolean mCursorWasVisibleBeforeSuggestions;
+ private boolean mIsShowingUp = false;
+ private SuggestionAdapter mSuggestionsAdapter;
+ private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
+ private final HashMap<SuggestionSpan, Integer> mSpansLengths;
+
+ private class CustomPopupWindow extends PopupWindow {
+ public CustomPopupWindow(Context context, int defStyle) {
+ super(context, null, defStyle);
+ }
+
+ @Override
+ public void dismiss() {
+ super.dismiss();
+
+ getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
+
+ // Safe cast since show() checks that mTextView.getText() is an Editable
+ ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
+
+ mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
+ if (hasInsertionController()) {
+ getInsertionController().show();
+ }
+ }
+ }
+
+ public SuggestionsPopupWindow() {
+ mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+ mSuggestionSpanComparator = new SuggestionSpanComparator();
+ mSpansLengths = new HashMap<SuggestionSpan, Integer>();
+ }
+
+ @Override
+ protected void createPopupWindow() {
+ mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
+ com.android.internal.R.attr.textSuggestionsWindowStyle);
+ mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopupWindow.setFocusable(true);
+ mPopupWindow.setClippingEnabled(false);
+ }
+
+ @Override
+ protected void initContentView() {
+ ListView listView = new ListView(mTextView.getContext());
+ mSuggestionsAdapter = new SuggestionAdapter();
+ listView.setAdapter(mSuggestionsAdapter);
+ listView.setOnItemClickListener(this);
+ mContentView = listView;
+
+ // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
+ mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
+ for (int i = 0; i < mSuggestionInfos.length; i++) {
+ mSuggestionInfos[i] = new SuggestionInfo();
+ }
+ }
+
+ public boolean isShowingUp() {
+ return mIsShowingUp;
+ }
+
+ public void onParentLostFocus() {
+ mIsShowingUp = false;
+ }
+
+ private class SuggestionInfo {
+ int suggestionStart, suggestionEnd; // range of actual suggestion within text
+ SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
+ int suggestionIndex; // the index of this suggestion inside suggestionSpan
+ SpannableStringBuilder text = new SpannableStringBuilder();
+ TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(),
+ android.R.style.TextAppearance_SuggestionHighlight);
+ }
+
+ private class SuggestionAdapter extends BaseAdapter {
+ private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ @Override
+ public int getCount() {
+ return mNumberOfSuggestions;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mSuggestionInfos[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView textView = (TextView) convertView;
+
+ if (textView == null) {
+ textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
+ parent, false);
+ }
+
+ final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+ textView.setText(suggestionInfo.text);
+
+ if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
+ textView.setCompoundDrawablesWithIntrinsicBounds(
+ com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
+ } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
+ textView.setCompoundDrawablesWithIntrinsicBounds(
+ com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
+ } else {
+ textView.setCompoundDrawables(null, null, null, null);
+ }
+
+ return textView;
+ }
+ }
+
+ private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
+ public int compare(SuggestionSpan span1, SuggestionSpan span2) {
+ final int flag1 = span1.getFlags();
+ final int flag2 = span2.getFlags();
+ if (flag1 != flag2) {
+ // The order here should match what is used in updateDrawState
+ final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
+ final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
+ final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
+ final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
+ if (easy1 && !misspelled1) return -1;
+ if (easy2 && !misspelled2) return 1;
+ if (misspelled1) return -1;
+ if (misspelled2) return 1;
+ }
+
+ return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
+ }
+ }
+
+ /**
+ * Returns the suggestion spans that cover the current cursor position. The suggestion
+ * spans are sorted according to the length of text that they are attached to.
+ */
+ private SuggestionSpan[] getSuggestionSpans() {
+ int pos = mTextView.getSelectionStart();
+ Spannable spannable = (Spannable) mTextView.getText();
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
+
+ mSpansLengths.clear();
+ for (SuggestionSpan suggestionSpan : suggestionSpans) {
+ int start = spannable.getSpanStart(suggestionSpan);
+ int end = spannable.getSpanEnd(suggestionSpan);
+ mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
+ }
+
+ // The suggestions are sorted according to their types (easy correction first, then
+ // misspelled) and to the length of the text that they cover (shorter first).
+ Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
+ return suggestionSpans;
+ }
+
+ @Override
+ public void show() {
+ if (!(mTextView.getText() instanceof Editable)) return;
+
+ if (updateSuggestions()) {
+ mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+ mTextView.setCursorVisible(false);
+ mIsShowingUp = true;
+ super.show();
+ }
+ }
+
+ @Override
+ protected void measureContent() {
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
+ displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
+ final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
+ displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
+
+ int width = 0;
+ View view = null;
+ for (int i = 0; i < mNumberOfSuggestions; i++) {
+ view = mSuggestionsAdapter.getView(i, view, mContentView);
+ view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
+ view.measure(horizontalMeasure, verticalMeasure);
+ width = Math.max(width, view.getMeasuredWidth());
+ }
+
+ // Enforce the width based on actual text widths
+ mContentView.measure(
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ verticalMeasure);
+
+ Drawable popupBackground = mPopupWindow.getBackground();
+ if (popupBackground != null) {
+ if (mTempRect == null) mTempRect = new Rect();
+ popupBackground.getPadding(mTempRect);
+ width += mTempRect.left + mTempRect.right;
+ }
+ mPopupWindow.setWidth(width);
+ }
+
+ @Override
+ protected int getTextOffset() {
+ return mTextView.getSelectionStart();
+ }
+
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ return mTextView.getLayout().getLineBottom(line);
+ }
+
+ @Override
+ protected int clipVertically(int positionY) {
+ final int height = mContentView.getMeasuredHeight();
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ return Math.min(positionY, displayMetrics.heightPixels - height);
+ }
+
+ @Override
+ public void hide() {
+ super.hide();
+ }
+
+ private boolean updateSuggestions() {
+ Spannable spannable = (Spannable) mTextView.getText();
+ SuggestionSpan[] suggestionSpans = getSuggestionSpans();
+
+ final int nbSpans = suggestionSpans.length;
+ // Suggestions are shown after a delay: the underlying spans may have been removed
+ if (nbSpans == 0) return false;
+
+ mNumberOfSuggestions = 0;
+ int spanUnionStart = mTextView.getText().length();
+ int spanUnionEnd = 0;
+
+ SuggestionSpan misspelledSpan = null;
+ int underlineColor = 0;
+
+ for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
+ SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
+ final int spanStart = spannable.getSpanStart(suggestionSpan);
+ final int spanEnd = spannable.getSpanEnd(suggestionSpan);
+ spanUnionStart = Math.min(spanStart, spanUnionStart);
+ spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
+
+ if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+ misspelledSpan = suggestionSpan;
+ }
+
+ // The first span dictates the background color of the highlighted text
+ if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
+
+ String[] suggestions = suggestionSpan.getSuggestions();
+ int nbSuggestions = suggestions.length;
+ for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
+ String suggestion = suggestions[suggestionIndex];
+
+ boolean suggestionIsDuplicate = false;
+ for (int i = 0; i < mNumberOfSuggestions; i++) {
+ if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
+ SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
+ final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
+ final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
+ if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
+ suggestionIsDuplicate = true;
+ break;
+ }
+ }
+ }
+
+ if (!suggestionIsDuplicate) {
+ SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
+ suggestionInfo.suggestionSpan = suggestionSpan;
+ suggestionInfo.suggestionIndex = suggestionIndex;
+ suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
+
+ mNumberOfSuggestions++;
+
+ if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
+ // Also end outer for loop
+ spanIndex = nbSpans;
+ break;
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < mNumberOfSuggestions; i++) {
+ highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
+ }
+
+ // Add "Add to dictionary" item if there is a span with the misspelled flag
+ if (misspelledSpan != null) {
+ final int misspelledStart = spannable.getSpanStart(misspelledSpan);
+ final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
+ if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
+ SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
+ suggestionInfo.suggestionSpan = misspelledSpan;
+ suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
+ suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView.
+ getContext().getString(com.android.internal.R.string.addToDictionary));
+ suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ mNumberOfSuggestions++;
+ }
+ }
+
+ // Delete item
+ SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
+ suggestionInfo.suggestionSpan = null;
+ suggestionInfo.suggestionIndex = DELETE_TEXT;
+ suggestionInfo.text.replace(0, suggestionInfo.text.length(),
+ mTextView.getContext().getString(com.android.internal.R.string.deleteText));
+ suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mNumberOfSuggestions++;
+
+ if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
+ if (underlineColor == 0) {
+ // Fallback on the default highlight color when the first span does not provide one
+ mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
+ } else {
+ final float BACKGROUND_TRANSPARENCY = 0.4f;
+ final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
+ mSuggestionRangeSpan.setBackgroundColor(
+ (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
+ }
+ spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ mSuggestionsAdapter.notifyDataSetChanged();
+ return true;
+ }
+
+ private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
+ int unionEnd) {
+ final Spannable text = (Spannable) mTextView.getText();
+ final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
+ final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
+
+ // Adjust the start/end of the suggestion span
+ suggestionInfo.suggestionStart = spanStart - unionStart;
+ suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
+ + suggestionInfo.text.length();
+
+ suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
+ suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ // Add the text before and after the span.
+ final String textAsString = text.toString();
+ suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
+ suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Editable editable = (Editable) mTextView.getText();
+ SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+
+ if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
+ final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
+ int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
+ if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
+ // Do not leave two adjacent spaces after deletion, or one at beginning of text
+ if (spanUnionEnd < editable.length() &&
+ Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
+ (spanUnionStart == 0 ||
+ Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
+ spanUnionEnd = spanUnionEnd + 1;
+ }
+ mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
+ }
+ hide();
+ return;
+ }
+
+ final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
+ final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
+ if (spanStart < 0 || spanEnd <= spanStart) {
+ // Span has been removed
+ hide();
+ return;
+ }
+
+ final String originalText = editable.toString().substring(spanStart, spanEnd);
+
+ if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
+ Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
+ intent.putExtra("word", originalText);
+ intent.putExtra("locale", mTextView.getTextServicesLocale().toString());
+ intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
+ mTextView.getContext().startActivity(intent);
+ // There is no way to know if the word was indeed added. Re-check.
+ // TODO The ExtractEditText should remove the span in the original text instead
+ editable.removeSpan(suggestionInfo.suggestionSpan);
+ updateSpellCheckSpans(spanStart, spanEnd, false);
+ } else {
+ // SuggestionSpans are removed by replace: save them before
+ SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
+ SuggestionSpan.class);
+ final int length = suggestionSpans.length;
+ int[] suggestionSpansStarts = new int[length];
+ int[] suggestionSpansEnds = new int[length];
+ int[] suggestionSpansFlags = new int[length];
+ for (int i = 0; i < length; i++) {
+ final SuggestionSpan suggestionSpan = suggestionSpans[i];
+ suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
+ suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
+ suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
+
+ // Remove potential misspelled flags
+ int suggestionSpanFlags = suggestionSpan.getFlags();
+ if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
+ suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
+ suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
+ suggestionSpan.setFlags(suggestionSpanFlags);
+ }
+ }
+
+ final int suggestionStart = suggestionInfo.suggestionStart;
+ final int suggestionEnd = suggestionInfo.suggestionEnd;
+ final String suggestion = suggestionInfo.text.subSequence(
+ suggestionStart, suggestionEnd).toString();
+ mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
+
+ // Notify source IME of the suggestion pick. Do this before swaping texts.
+ if (!TextUtils.isEmpty(
+ suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
+ suggestionInfo.suggestionIndex);
+ }
+ }
+
+ // Swap text content between actual text and Suggestion span
+ String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
+ suggestions[suggestionInfo.suggestionIndex] = originalText;
+
+ // Restore previous SuggestionSpans
+ final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
+ for (int i = 0; i < length; i++) {
+ // Only spans that include the modified region make sense after replacement
+ // Spans partially included in the replaced region are removed, there is no
+ // way to assign them a valid range after replacement
+ if (suggestionSpansStarts[i] <= spanStart &&
+ suggestionSpansEnds[i] >= spanEnd) {
+ mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
+ suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
+ }
+ }
+
+ // Move cursor at the end of the replaced word
+ final int newCursorPosition = spanEnd + lengthDifference;
+ mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
+ }
+
+ hide();
+ }
+ }
+
+ /**
+ * An ActionMode Callback class that is used to provide actions while in text selection mode.
+ *
+ * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
+ * on which of these this TextView supports.
+ */
+ private class SelectionActionModeCallback implements ActionMode.Callback {
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes(
+ com.android.internal.R.styleable.SelectionModeDrawables);
+
+ boolean allowText = mTextView.getContext().getResources().getBoolean(
+ com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
+
+ mode.setTitle(mTextView.getContext().getString(
+ com.android.internal.R.string.textSelectionCABTitle));
+ mode.setSubtitle(null);
+ mode.setTitleOptionalHint(true);
+
+ int selectAllIconId = 0; // No icon by default
+ if (!allowText) {
+ // Provide an icon, text will not be displayed on smaller screens.
+ selectAllIconId = styledAttributes.getResourceId(
+ R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
+ }
+
+ menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
+ setIcon(selectAllIconId).
+ setAlphabeticShortcut('a').
+ setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ if (mTextView.canCut()) {
+ menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut).
+ setIcon(styledAttributes.getResourceId(
+ R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
+ setAlphabeticShortcut('x').
+ setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ if (mTextView.canCopy()) {
+ menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy).
+ setIcon(styledAttributes.getResourceId(
+ R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
+ setAlphabeticShortcut('c').
+ setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ if (mTextView.canPaste()) {
+ menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste).
+ setIcon(styledAttributes.getResourceId(
+ R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
+ setAlphabeticShortcut('v').
+ setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ styledAttributes.recycle();
+
+ if (mCustomSelectionActionModeCallback != null) {
+ if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
+ // The custom mode can choose to cancel the action mode
+ return false;
+ }
+ }
+
+ if (menu.hasVisibleItems() || mode.getCustomView() != null) {
+ getSelectionController().show();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ if (mCustomSelectionActionModeCallback != null) {
+ return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (mCustomSelectionActionModeCallback != null &&
+ mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
+ return true;
+ }
+ return mTextView.onTextContextMenuItem(item.getItemId());
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ if (mCustomSelectionActionModeCallback != null) {
+ mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
+ }
+ Selection.setSelection((Spannable) mTextView.getText(), mTextView.getSelectionEnd());
+
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.hide();
+ }
+
+ mSelectionActionMode = null;
+ }
+ }
+
+ private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
+ private static final int POPUP_TEXT_LAYOUT =
+ com.android.internal.R.layout.text_edit_action_popup_text;
+ private TextView mPasteTextView;
+ private TextView mReplaceTextView;
+
+ @Override
+ protected void createPopupWindow() {
+ mPopupWindow = new PopupWindow(mTextView.getContext(), null,
+ com.android.internal.R.attr.textSelectHandleWindowStyle);
+ mPopupWindow.setClippingEnabled(true);
+ }
+
+ @Override
+ protected void initContentView() {
+ LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
+ linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ mContentView = linearLayout;
+ mContentView.setBackgroundResource(
+ com.android.internal.R.drawable.text_edit_paste_window);
+
+ LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LayoutParams wrapContent = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+ mPasteTextView.setLayoutParams(wrapContent);
+ mContentView.addView(mPasteTextView);
+ mPasteTextView.setText(com.android.internal.R.string.paste);
+ mPasteTextView.setOnClickListener(this);
+
+ mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+ mReplaceTextView.setLayoutParams(wrapContent);
+ mContentView.addView(mReplaceTextView);
+ mReplaceTextView.setText(com.android.internal.R.string.replace);
+ mReplaceTextView.setOnClickListener(this);
+ }
+
+ @Override
+ public void show() {
+ boolean canPaste = mTextView.canPaste();
+ boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
+ mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
+ mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
+
+ if (!canPaste && !canSuggest) return;
+
+ super.show();
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == mPasteTextView && mTextView.canPaste()) {
+ mTextView.onTextContextMenuItem(TextView.ID_PASTE);
+ hide();
+ } else if (view == mReplaceTextView) {
+ int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
+ stopSelectionActionMode();
+ Selection.setSelection((Spannable) mTextView.getText(), middle);
+ showSuggestions();
+ }
+ }
+
+ @Override
+ protected int getTextOffset() {
+ return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
+ }
+
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight();
+ }
+
+ @Override
+ protected int clipVertically(int positionY) {
+ if (positionY < 0) {
+ final int offset = getTextOffset();
+ final Layout layout = mTextView.getLayout();
+ final int line = layout.getLineForOffset(offset);
+ positionY += layout.getLineBottom(line) - layout.getLineTop(line);
+ positionY += mContentView.getMeasuredHeight();
+
+ // Assumes insertion and selection handles share the same height
+ final Drawable handle = mTextView.getResources().getDrawable(
+ mTextView.mTextSelectHandleRes);
+ positionY += handle.getIntrinsicHeight();
+ }
+
+ return positionY;
+ }
+ }
+
+ private abstract class HandleView extends View implements TextViewPositionListener {
+ protected Drawable mDrawable;
+ protected Drawable mDrawableLtr;
+ protected Drawable mDrawableRtl;
+ private final PopupWindow mContainer;
+ // Position with respect to the parent TextView
+ private int mPositionX, mPositionY;
+ private boolean mIsDragging;
+ // Offset from touch position to mPosition
+ private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
+ protected int mHotspotX;
+ // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
+ private float mTouchOffsetY;
+ // Where the touch position should be on the handle to ensure a maximum cursor visibility
+ private float mIdealVerticalOffset;
+ // Parent's (TextView) previous position in window
+ private int mLastParentX, mLastParentY;
+ // Transient action popup window for Paste and Replace actions
+ protected ActionPopupWindow mActionPopupWindow;
+ // Previous text character offset
+ private int mPreviousOffset = -1;
+ // Previous text character offset
+ private boolean mPositionHasChanged = true;
+ // Used to delay the appearance of the action popup window
+ private Runnable mActionPopupShower;
+
+ public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
+ super(mTextView.getContext());
+ mContainer = new PopupWindow(mTextView.getContext(), null,
+ com.android.internal.R.attr.textSelectHandleWindowStyle);
+ mContainer.setSplitTouchEnabled(true);
+ mContainer.setClippingEnabled(false);
+ mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+ mContainer.setContentView(this);
+
+ mDrawableLtr = drawableLtr;
+ mDrawableRtl = drawableRtl;
+
+ updateDrawable();
+
+ final int handleHeight = mDrawable.getIntrinsicHeight();
+ mTouchOffsetY = -0.3f * handleHeight;
+ mIdealVerticalOffset = 0.7f * handleHeight;
+ }
+
+ protected void updateDrawable() {
+ final int offset = getCurrentCursorOffset();
+ final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
+ mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
+ mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
+ }
+
+ protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
+
+ // Touch-up filter: number of previous positions remembered
+ private static final int HISTORY_SIZE = 5;
+ private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
+ private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
+ private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
+ private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
+ private int mPreviousOffsetIndex = 0;
+ private int mNumberPreviousOffsets = 0;
+
+ private void startTouchUpFilter(int offset) {
+ mNumberPreviousOffsets = 0;
+ addPositionToTouchUpFilter(offset);
+ }
+
+ private void addPositionToTouchUpFilter(int offset) {
+ mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
+ mPreviousOffsets[mPreviousOffsetIndex] = offset;
+ mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
+ mNumberPreviousOffsets++;
+ }
+
+ private void filterOnTouchUp() {
+ final long now = SystemClock.uptimeMillis();
+ int i = 0;
+ int index = mPreviousOffsetIndex;
+ final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
+ while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
+ i++;
+ index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
+ }
+
+ if (i > 0 && i < iMax &&
+ (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
+ positionAtCursorOffset(mPreviousOffsets[index], false);
+ }
+ }
+
+ public boolean offsetHasBeenChanged() {
+ return mNumberPreviousOffsets > 1;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
+ }
+
+ public void show() {
+ if (isShowing()) return;
+
+ getPositionListener().addSubscriber(this, true /* local position may change */);
+
+ // Make sure the offset is always considered new, even when focusing at same position
+ mPreviousOffset = -1;
+ positionAtCursorOffset(getCurrentCursorOffset(), false);
+
+ hideActionPopupWindow();
+ }
+
+ protected void dismiss() {
+ mIsDragging = false;
+ mContainer.dismiss();
+ onDetached();
+ }
+
+ public void hide() {
+ dismiss();
+
+ getPositionListener().removeSubscriber(this);
+ }
+
+ void showActionPopupWindow(int delay) {
+ if (mActionPopupWindow == null) {
+ mActionPopupWindow = new ActionPopupWindow();
+ }
+ if (mActionPopupShower == null) {
+ mActionPopupShower = new Runnable() {
+ public void run() {
+ mActionPopupWindow.show();
+ }
+ };
+ } else {
+ mTextView.removeCallbacks(mActionPopupShower);
+ }
+ mTextView.postDelayed(mActionPopupShower, delay);
+ }
+
+ protected void hideActionPopupWindow() {
+ if (mActionPopupShower != null) {
+ mTextView.removeCallbacks(mActionPopupShower);
+ }
+ if (mActionPopupWindow != null) {
+ mActionPopupWindow.hide();
+ }
+ }
+
+ public boolean isShowing() {
+ return mContainer.isShowing();
+ }
+
+ private boolean isVisible() {
+ // Always show a dragging handle.
+ if (mIsDragging) {
+ return true;
+ }
+
+ if (mTextView.isInBatchEditMode()) {
+ return false;
+ }
+
+ return isPositionVisible(mPositionX + mHotspotX, mPositionY);
+ }
+
+ public abstract int getCurrentCursorOffset();
+
+ protected abstract void updateSelection(int offset);
+
+ public abstract void updatePosition(float x, float y);
+
+ protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
+ // A HandleView relies on the layout, which may be nulled by external methods
+ Layout layout = mTextView.getLayout();
+ if (layout == null) {
+ // Will update controllers' state, hiding them and stopping selection mode if needed
+ prepareCursorControllers();
+ return;
+ }
+
+ boolean offsetChanged = offset != mPreviousOffset;
+ if (offsetChanged || parentScrolled) {
+ if (offsetChanged) {
+ updateSelection(offset);
+ addPositionToTouchUpFilter(offset);
+ }
+ final int line = layout.getLineForOffset(offset);
+
+ mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
+ mPositionY = layout.getLineBottom(line);
+
+ // Take TextView's padding and scroll into account.
+ mPositionX += mTextView.viewportToContentHorizontalOffset();
+ mPositionY += mTextView.viewportToContentVerticalOffset();
+
+ mPreviousOffset = offset;
+ mPositionHasChanged = true;
+ }
+ }
+
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled) {
+ positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
+ if (parentPositionChanged || mPositionHasChanged) {
+ if (mIsDragging) {
+ // Update touchToWindow offset in case of parent scrolling while dragging
+ if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
+ mTouchToWindowOffsetX += parentPositionX - mLastParentX;
+ mTouchToWindowOffsetY += parentPositionY - mLastParentY;
+ mLastParentX = parentPositionX;
+ mLastParentY = parentPositionY;
+ }
+
+ onHandleMoved();
+ }
+
+ if (isVisible()) {
+ final int positionX = parentPositionX + mPositionX;
+ final int positionY = parentPositionY + mPositionY;
+ if (isShowing()) {
+ mContainer.update(positionX, positionY, -1, -1);
+ } else {
+ mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+ positionX, positionY);
+ }
+ } else {
+ if (isShowing()) {
+ dismiss();
+ }
+ }
+
+ mPositionHasChanged = false;
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
+ mDrawable.draw(c);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ startTouchUpFilter(getCurrentCursorOffset());
+ mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
+ mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
+
+ final PositionListener positionListener = getPositionListener();
+ mLastParentX = positionListener.getPositionX();
+ mLastParentY = positionListener.getPositionY();
+ mIsDragging = true;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final float rawX = ev.getRawX();
+ final float rawY = ev.getRawY();
+
+ // Vertical hysteresis: vertical down movement tends to snap to ideal offset
+ final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
+ final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
+ float newVerticalOffset;
+ if (previousVerticalOffset < mIdealVerticalOffset) {
+ newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
+ newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
+ } else {
+ newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
+ newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
+ }
+ mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
+
+ final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
+ final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
+
+ updatePosition(newPosX, newPosY);
+ break;
+ }
+
+ case MotionEvent.ACTION_UP:
+ filterOnTouchUp();
+ mIsDragging = false;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mIsDragging = false;
+ break;
+ }
+ return true;
+ }
+
+ public boolean isDragging() {
+ return mIsDragging;
+ }
+
+ void onHandleMoved() {
+ hideActionPopupWindow();
+ }
+
+ public void onDetached() {
+ hideActionPopupWindow();
+ }
+ }
+
+ private class InsertionHandleView extends HandleView {
+ private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
+ private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
+
+ // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
+ private float mDownPositionX, mDownPositionY;
+ private Runnable mHider;
+
+ public InsertionHandleView(Drawable drawable) {
+ super(drawable, drawable);
+ }
+
+ @Override
+ public void show() {
+ super.show();
+
+ final long durationSinceCutOrCopy =
+ SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME;
+ if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
+ showActionPopupWindow(0);
+ }
+
+ hideAfterDelay();
+ }
+
+ public void showWithActionPopup() {
+ show();
+ showActionPopupWindow(0);
+ }
+
+ private void hideAfterDelay() {
+ if (mHider == null) {
+ mHider = new Runnable() {
+ public void run() {
+ hide();
+ }
+ };
+ } else {
+ removeHiderCallback();
+ }
+ mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
+ }
+
+ private void removeHiderCallback() {
+ if (mHider != null) {
+ mTextView.removeCallbacks(mHider);
+ }
+ }
+
+ @Override
+ protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+ return drawable.getIntrinsicWidth() / 2;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final boolean result = super.onTouchEvent(ev);
+
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownPositionX = ev.getRawX();
+ mDownPositionY = ev.getRawY();
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (!offsetHasBeenChanged()) {
+ final float deltaX = mDownPositionX - ev.getRawX();
+ final float deltaY = mDownPositionY - ev.getRawY();
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ mTextView.getContext());
+ final int touchSlop = viewConfiguration.getScaledTouchSlop();
+
+ if (distanceSquared < touchSlop * touchSlop) {
+ if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
+ // Tapping on the handle dismisses the displayed action popup
+ mActionPopupWindow.hide();
+ } else {
+ showWithActionPopup();
+ }
+ }
+ }
+ hideAfterDelay();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ hideAfterDelay();
+ break;
+
+ default:
+ break;
+ }
+
+ return result;
+ }
+
+ @Override
+ public int getCurrentCursorOffset() {
+ return mTextView.getSelectionStart();
+ }
+
+ @Override
+ public void updateSelection(int offset) {
+ Selection.setSelection((Spannable) mTextView.getText(), offset);
+ }
+
+ @Override
+ public void updatePosition(float x, float y) {
+ positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
+ }
+
+ @Override
+ void onHandleMoved() {
+ super.onHandleMoved();
+ removeHiderCallback();
+ }
+
+ @Override
+ public void onDetached() {
+ super.onDetached();
+ removeHiderCallback();
+ }
+ }
+
+ private class SelectionStartHandleView extends HandleView {
+
+ public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
+ super(drawableLtr, drawableRtl);
+ }
+
+ @Override
+ protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+ if (isRtlRun) {
+ return drawable.getIntrinsicWidth() / 4;
+ } else {
+ return (drawable.getIntrinsicWidth() * 3) / 4;
+ }
+ }
+
+ @Override
+ public int getCurrentCursorOffset() {
+ return mTextView.getSelectionStart();
+ }
+
+ @Override
+ public void updateSelection(int offset) {
+ Selection.setSelection((Spannable) mTextView.getText(), offset,
+ mTextView.getSelectionEnd());
+ updateDrawable();
+ }
+
+ @Override
+ public void updatePosition(float x, float y) {
+ int offset = mTextView.getOffsetForPosition(x, y);
+
+ // Handles can not cross and selection is at least one character
+ final int selectionEnd = mTextView.getSelectionEnd();
+ if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
+
+ positionAtCursorOffset(offset, false);
+ }
+
+ public ActionPopupWindow getActionPopupWindow() {
+ return mActionPopupWindow;
+ }
+ }
+
+ private class SelectionEndHandleView extends HandleView {
+
+ public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
+ super(drawableLtr, drawableRtl);
+ }
+
+ @Override
+ protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+ if (isRtlRun) {
+ return (drawable.getIntrinsicWidth() * 3) / 4;
+ } else {
+ return drawable.getIntrinsicWidth() / 4;
+ }
+ }
+
+ @Override
+ public int getCurrentCursorOffset() {
+ return mTextView.getSelectionEnd();
+ }
+
+ @Override
+ public void updateSelection(int offset) {
+ Selection.setSelection((Spannable) mTextView.getText(),
+ mTextView.getSelectionStart(), offset);
+ updateDrawable();
+ }
+
+ @Override
+ public void updatePosition(float x, float y) {
+ int offset = mTextView.getOffsetForPosition(x, y);
+
+ // Handles can not cross and selection is at least one character
+ final int selectionStart = mTextView.getSelectionStart();
+ if (offset <= selectionStart) {
+ offset = Math.min(selectionStart + 1, mTextView.getText().length());
+ }
+
+ positionAtCursorOffset(offset, false);
+ }
+
+ public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
+ mActionPopupWindow = actionPopupWindow;
+ }
+ }
+
+ /**
+ * A CursorController instance can be used to control a cursor in the text.
+ */
+ private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
+ /**
+ * Makes the cursor controller visible on screen.
+ * See also {@link #hide()}.
+ */
+ public void show();
+
+ /**
+ * Hide the cursor controller from screen.
+ * See also {@link #show()}.
+ */
+ public void hide();
+
+ /**
+ * Called when the view is detached from window. Perform house keeping task, such as
+ * stopping Runnable thread that would otherwise keep a reference on the context, thus
+ * preventing the activity from being recycled.
+ */
+ public void onDetached();
+ }
+
+ private class InsertionPointCursorController implements CursorController {
+ private InsertionHandleView mHandle;
+
+ public void show() {
+ getHandle().show();
+ }
+
+ public void showWithActionPopup() {
+ getHandle().showWithActionPopup();
+ }
+
+ public void hide() {
+ if (mHandle != null) {
+ mHandle.hide();
+ }
+ }
+
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode) {
+ hide();
+ }
+ }
+
+ private InsertionHandleView getHandle() {
+ if (mSelectHandleCenter == null) {
+ mSelectHandleCenter = mTextView.getResources().getDrawable(
+ mTextView.mTextSelectHandleRes);
+ }
+ if (mHandle == null) {
+ mHandle = new InsertionHandleView(mSelectHandleCenter);
+ }
+ return mHandle;
+ }
+
+ @Override
+ public void onDetached() {
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.removeOnTouchModeChangeListener(this);
+
+ if (mHandle != null) mHandle.onDetached();
+ }
+ }
+
+ class SelectionModifierCursorController implements CursorController {
+ private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
+ // The cursor controller handles, lazily created when shown.
+ private SelectionStartHandleView mStartHandle;
+ private SelectionEndHandleView mEndHandle;
+ // The offsets of that last touch down event. Remembered to start selection there.
+ private int mMinTouchOffset, mMaxTouchOffset;
+
+ // Double tap detection
+ private long mPreviousTapUpTime = 0;
+ private float mDownPositionX, mDownPositionY;
+ private boolean mGestureStayedInTapRegion;
+
+ SelectionModifierCursorController() {
+ resetTouchOffsets();
+ }
+
+ public void show() {
+ if (mTextView.isInBatchEditMode()) {
+ return;
+ }
+ initDrawables();
+ initHandles();
+ hideInsertionPointCursorController();
+ }
+
+ private void initDrawables() {
+ if (mSelectHandleLeft == null) {
+ mSelectHandleLeft = mTextView.getContext().getResources().getDrawable(
+ mTextView.mTextSelectHandleLeftRes);
+ }
+ if (mSelectHandleRight == null) {
+ mSelectHandleRight = mTextView.getContext().getResources().getDrawable(
+ mTextView.mTextSelectHandleRightRes);
+ }
+ }
+
+ private void initHandles() {
+ // Lazy object creation has to be done before updatePosition() is called.
+ if (mStartHandle == null) {
+ mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
+ }
+ if (mEndHandle == null) {
+ mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
+ }
+
+ mStartHandle.show();
+ mEndHandle.show();
+
+ // Make sure both left and right handles share the same ActionPopupWindow (so that
+ // moving any of the handles hides the action popup).
+ mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
+ mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
+
+ hideInsertionPointCursorController();
+ }
+
+ public void hide() {
+ if (mStartHandle != null) mStartHandle.hide();
+ if (mEndHandle != null) mEndHandle.hide();
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ // This is done even when the View does not have focus, so that long presses can start
+ // selection and tap can move cursor from this tap position.
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ final float x = event.getX();
+ final float y = event.getY();
+
+ // Remember finger down position, to be able to start selection from there
+ mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
+
+ // Double tap detection
+ if (mGestureStayedInTapRegion) {
+ long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
+ if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
+ final float deltaX = x - mDownPositionX;
+ final float deltaY = y - mDownPositionY;
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ mTextView.getContext());
+ int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
+ boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
+
+ if (stayedInArea && isPositionOnText(x, y)) {
+ startSelectionActionMode();
+ mDiscardNextActionUp = true;
+ }
+ }
+ }
+
+ mDownPositionX = x;
+ mDownPositionY = y;
+ mGestureStayedInTapRegion = true;
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_POINTER_UP:
+ // Handle multi-point gestures. Keep min and max offset positions.
+ // Only activated for devices that correctly handle multi-touch.
+ if (mTextView.getContext().getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
+ updateMinAndMaxOffsets(event);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mGestureStayedInTapRegion) {
+ final float deltaX = event.getX() - mDownPositionX;
+ final float deltaY = event.getY() - mDownPositionY;
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ mTextView.getContext());
+ int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
+
+ if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
+ mGestureStayedInTapRegion = false;
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ mPreviousTapUpTime = SystemClock.uptimeMillis();
+ break;
+ }
+ }
+
+ /**
+ * @param event
+ */
+ private void updateMinAndMaxOffsets(MotionEvent event) {
+ int pointerCount = event.getPointerCount();
+ for (int index = 0; index < pointerCount; index++) {
+ int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
+ if (offset < mMinTouchOffset) mMinTouchOffset = offset;
+ if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
+ }
+ }
+
+ public int getMinTouchOffset() {
+ return mMinTouchOffset;
+ }
+
+ public int getMaxTouchOffset() {
+ return mMaxTouchOffset;
+ }
+
+ public void resetTouchOffsets() {
+ mMinTouchOffset = mMaxTouchOffset = -1;
+ }
+
+ /**
+ * @return true iff this controller is currently used to move the selection start.
+ */
+ public boolean isSelectionStartDragged() {
+ return mStartHandle != null && mStartHandle.isDragging();
+ }
+
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode) {
+ hide();
+ }
+ }
+
+ @Override
+ public void onDetached() {
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.removeOnTouchModeChangeListener(this);
+
+ if (mStartHandle != null) mStartHandle.onDetached();
+ if (mEndHandle != null) mEndHandle.onDetached();
+ }
+ }
+
+ private class CorrectionHighlighter {
+ private final Path mPath = new Path();
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private int mStart, mEnd;
+ private long mFadingStartTime;
+ private RectF mTempRectF;
+ private final static int FADE_OUT_DURATION = 400;
+
+ public CorrectionHighlighter() {
+ mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
+ applicationScale);
+ mPaint.setStyle(Paint.Style.FILL);
+ }
+
+ public void highlight(CorrectionInfo info) {
+ mStart = info.getOffset();
+ mEnd = mStart + info.getNewText().length();
+ mFadingStartTime = SystemClock.uptimeMillis();
+
+ if (mStart < 0 || mEnd < 0) {
+ stopAnimation();
+ }
+ }
+
+ public void draw(Canvas canvas, int cursorOffsetVertical) {
+ if (updatePath() && updatePaint()) {
+ if (cursorOffsetVertical != 0) {
+ canvas.translate(0, cursorOffsetVertical);
+ }
+
+ canvas.drawPath(mPath, mPaint);
+
+ if (cursorOffsetVertical != 0) {
+ canvas.translate(0, -cursorOffsetVertical);
+ }
+ invalidate(true); // TODO invalidate cursor region only
+ } else {
+ stopAnimation();
+ invalidate(false); // TODO invalidate cursor region only
+ }
+ }
+
+ private boolean updatePaint() {
+ final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
+ if (duration > FADE_OUT_DURATION) return false;
+
+ final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
+ final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
+ final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
+ ((int) (highlightColorAlpha * coef) << 24);
+ mPaint.setColor(color);
+ return true;
+ }
+
+ private boolean updatePath() {
+ final Layout layout = mTextView.getLayout();
+ if (layout == null) return false;
+
+ // Update in case text is edited while the animation is run
+ final int length = mTextView.getText().length();
+ int start = Math.min(length, mStart);
+ int end = Math.min(length, mEnd);
+
+ mPath.reset();
+ layout.getSelectionPath(start, end, mPath);
+ return true;
+ }
+
+ private void invalidate(boolean delayed) {
+ if (mTextView.getLayout() == null) return;
+
+ if (mTempRectF == null) mTempRectF = new RectF();
+ mPath.computeBounds(mTempRectF, false);
+
+ int left = mTextView.getCompoundPaddingLeft();
+ int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
+
+ if (delayed) {
+ mTextView.postInvalidateOnAnimation(
+ left + (int) mTempRectF.left, top + (int) mTempRectF.top,
+ left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
+ } else {
+ mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
+ (int) mTempRectF.right, (int) mTempRectF.bottom);
+ }
+ }
+
+ private void stopAnimation() {
+ Editor.this.mCorrectionHighlighter = null;
+ }
+ }
+
+ private static class ErrorPopup extends PopupWindow {
+ private boolean mAbove = false;
+ private final TextView mView;
+ private int mPopupInlineErrorBackgroundId = 0;
+ private int mPopupInlineErrorAboveBackgroundId = 0;
+
+ ErrorPopup(TextView v, int width, int height) {
+ super(v, width, height);
+ mView = v;
+ // Make sure the TextView has a background set as it will be used the first time it is
+ // shown and positionned. Initialized with below background, which should have
+ // dimensions identical to the above version for this to work (and is more likely).
+ mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
+ com.android.internal.R.styleable.Theme_errorMessageBackground);
+ mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
+ }
+
+ void fixDirection(boolean above) {
+ mAbove = above;
+
+ if (above) {
+ mPopupInlineErrorAboveBackgroundId =
+ getResourceId(mPopupInlineErrorAboveBackgroundId,
+ com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
+ } else {
+ mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
+ com.android.internal.R.styleable.Theme_errorMessageBackground);
+ }
+
+ mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
+ mPopupInlineErrorBackgroundId);
+ }
+
+ private int getResourceId(int currentId, int index) {
+ if (currentId == 0) {
+ TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
+ R.styleable.Theme);
+ currentId = styledAttributes.getResourceId(index, 0);
+ styledAttributes.recycle();
+ }
+ return currentId;
+ }
+
+ @Override
+ public void update(int x, int y, int w, int h, boolean force) {
+ super.update(x, y, w, h, force);
+
+ boolean above = isAboveAnchor();
+ if (above != mAbove) {
+ fixDirection(above);
+ }
+ }
+ }
+
+ static class InputContentType {
+ int imeOptions = EditorInfo.IME_NULL;
+ String privateImeOptions;
+ CharSequence imeActionLabel;
+ int imeActionId;
+ Bundle extras;
+ OnEditorActionListener onEditorActionListener;
+ boolean enterDown;
+ }
+
+ static class InputMethodState {
+ Rect mCursorRectInWindow = new Rect();
+ RectF mTmpRectF = new RectF();
+ float[] mTmpOffset = new float[2];
+ ExtractedTextRequest mExtracting;
+ final ExtractedText mTmpExtracted = new ExtractedText();
+ int mBatchEditNesting;
+ boolean mCursorChanged;
+ boolean mSelectionModeChanged;
+ boolean mContentChanged;
+ int mChangedStart, mChangedEnd, mChangedDelta;
+ }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 1f2410b..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) {
@@ -5251,11 +5098,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- createEditorIfNeeded("onCreateInputConnection");
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;
@@ -5308,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
@@ -5437,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)}.
@@ -5493,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();
}
/**
@@ -5519,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();
}
/**
@@ -5645,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();
}
/**
@@ -5827,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,
@@ -6639,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);
@@ -6856,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 ||
@@ -7050,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();
}
@@ -7086,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;
@@ -7095,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();
}
}
@@ -7187,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
@@ -7220,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
@@ -7270,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();
}
}
@@ -7351,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;
}
@@ -7388,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) {
@@ -7451,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();
}
@@ -7523,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;
@@ -7727,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();
}
/**
@@ -7739,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.
@@ -7747,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.
*
@@ -7878,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
@@ -8005,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
@@ -8034,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:
@@ -8055,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;
@@ -8146,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.
@@ -8392,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);
}
/**
@@ -8490,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.
@@ -8536,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);
@@ -8545,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);
@@ -8559,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();
@@ -8589,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:
@@ -8599,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;
}
@@ -8865,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);
@@ -9042,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;
@@ -9323,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
@@ -9548,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);
@@ -9572,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);
@@ -9590,2263 +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 (mTextDisplayLists == null) {
- mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
- }
- if (! (layout instanceof DynamicLayout)) {
- Log.e(LOG_TAG, "Editable TextView is not using a DynamicLayout");
- return;
- }
-
- 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);
- }
-
- 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/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index 9a0ce3a..0524c25 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -188,178 +188,176 @@ public class PointerLocationView extends View {
@Override
protected void onDraw(Canvas canvas) {
- synchronized (mPointers) {
- final int w = getWidth();
- final int itemW = w/7;
- final int base = -mTextMetrics.ascent+1;
- final int bottom = mHeaderBottom;
-
- final int NP = mPointers.size();
+ final int w = getWidth();
+ final int itemW = w/7;
+ final int base = -mTextMetrics.ascent+1;
+ final int bottom = mHeaderBottom;
+
+ final int NP = mPointers.size();
+
+ // Labels
+ if (mActivePointerId >= 0) {
+ final PointerState ps = mPointers.get(mActivePointerId);
- // Labels
- if (mActivePointerId >= 0) {
- final PointerState ps = mPointers.get(mActivePointerId);
-
- canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint);
+ canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("P: ").append(mCurNumPointers)
+ .append(" / ").append(mMaxNumPointers)
+ .toString(), 1, base, mTextPaint);
+
+ final int N = ps.mTraceCount;
+ if ((mCurDown && ps.mCurDown) || N == 0) {
+ canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint);
canvas.drawText(mText.clear()
- .append("P: ").append(mCurNumPointers)
- .append(" / ").append(mMaxNumPointers)
- .toString(), 1, base, mTextPaint);
-
- final int N = ps.mTraceCount;
- if ((mCurDown && ps.mCurDown) || N == 0) {
- canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint);
- canvas.drawText(mText.clear()
- .append("X: ").append(ps.mCoords.x, 1)
- .toString(), 1 + itemW, base, mTextPaint);
- canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint);
- canvas.drawText(mText.clear()
- .append("Y: ").append(ps.mCoords.y, 1)
- .toString(), 1 + itemW * 2, base, mTextPaint);
- } else {
- float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
- float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
- canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom,
- Math.abs(dx) < mVC.getScaledTouchSlop()
- ? mTextBackgroundPaint : mTextLevelPaint);
- canvas.drawText(mText.clear()
- .append("dX: ").append(dx, 1)
- .toString(), 1 + itemW, base, mTextPaint);
- canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom,
- Math.abs(dy) < mVC.getScaledTouchSlop()
- ? mTextBackgroundPaint : mTextLevelPaint);
- canvas.drawText(mText.clear()
- .append("dY: ").append(dy, 1)
- .toString(), 1 + itemW * 2, base, mTextPaint);
- }
-
- canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint);
+ .append("X: ").append(ps.mCoords.x, 1)
+ .toString(), 1 + itemW, base, mTextPaint);
+ canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint);
canvas.drawText(mText.clear()
- .append("Xv: ").append(ps.mXVelocity, 3)
- .toString(), 1 + itemW * 3, base, mTextPaint);
-
- canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint);
- canvas.drawText(mText.clear()
- .append("Yv: ").append(ps.mYVelocity, 3)
- .toString(), 1 + itemW * 4, base, mTextPaint);
-
- canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint);
- canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1,
- bottom, mTextLevelPaint);
+ .append("Y: ").append(ps.mCoords.y, 1)
+ .toString(), 1 + itemW * 2, base, mTextPaint);
+ } else {
+ float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
+ float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
+ canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom,
+ Math.abs(dx) < mVC.getScaledTouchSlop()
+ ? mTextBackgroundPaint : mTextLevelPaint);
canvas.drawText(mText.clear()
- .append("Prs: ").append(ps.mCoords.pressure, 2)
- .toString(), 1 + itemW * 5, base, mTextPaint);
-
- canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint);
- canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1,
- bottom, mTextLevelPaint);
+ .append("dX: ").append(dx, 1)
+ .toString(), 1 + itemW, base, mTextPaint);
+ canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom,
+ Math.abs(dy) < mVC.getScaledTouchSlop()
+ ? mTextBackgroundPaint : mTextLevelPaint);
canvas.drawText(mText.clear()
- .append("Size: ").append(ps.mCoords.size, 2)
- .toString(), 1 + itemW * 6, base, mTextPaint);
+ .append("dY: ").append(dy, 1)
+ .toString(), 1 + itemW * 2, base, mTextPaint);
}
-
- // Pointer trace.
- for (int p = 0; p < NP; p++) {
- final PointerState ps = mPointers.get(p);
-
- // Draw path.
- final int N = ps.mTraceCount;
- float lastX = 0, lastY = 0;
- boolean haveLast = false;
- boolean drawn = false;
- mPaint.setARGB(255, 128, 255, 255);
- for (int i=0; i < N; i++) {
- float x = ps.mTraceX[i];
- float y = ps.mTraceY[i];
- if (Float.isNaN(x)) {
- haveLast = false;
- continue;
- }
- if (haveLast) {
- canvas.drawLine(lastX, lastY, x, y, mPathPaint);
- canvas.drawPoint(lastX, lastY, mPaint);
- drawn = true;
- }
- lastX = x;
- lastY = y;
- haveLast = true;
+
+ canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("Xv: ").append(ps.mXVelocity, 3)
+ .toString(), 1 + itemW * 3, base, mTextPaint);
+
+ canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("Yv: ").append(ps.mYVelocity, 3)
+ .toString(), 1 + itemW * 4, base, mTextPaint);
+
+ canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint);
+ canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1,
+ bottom, mTextLevelPaint);
+ canvas.drawText(mText.clear()
+ .append("Prs: ").append(ps.mCoords.pressure, 2)
+ .toString(), 1 + itemW * 5, base, mTextPaint);
+
+ canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint);
+ canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1,
+ bottom, mTextLevelPaint);
+ canvas.drawText(mText.clear()
+ .append("Size: ").append(ps.mCoords.size, 2)
+ .toString(), 1 + itemW * 6, base, mTextPaint);
+ }
+
+ // Pointer trace.
+ for (int p = 0; p < NP; p++) {
+ final PointerState ps = mPointers.get(p);
+
+ // Draw path.
+ final int N = ps.mTraceCount;
+ float lastX = 0, lastY = 0;
+ boolean haveLast = false;
+ boolean drawn = false;
+ mPaint.setARGB(255, 128, 255, 255);
+ for (int i=0; i < N; i++) {
+ float x = ps.mTraceX[i];
+ float y = ps.mTraceY[i];
+ if (Float.isNaN(x)) {
+ haveLast = false;
+ continue;
}
-
- if (drawn) {
- // Draw movement estimate curve.
- mPaint.setARGB(128, 128, 0, 128);
- float lx = ps.mEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
- float ly = ps.mEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
- for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {
- float x = ps.mEstimator.estimateX(i * ESTIMATE_INTERVAL);
- float y = ps.mEstimator.estimateY(i * ESTIMATE_INTERVAL);
- canvas.drawLine(lx, ly, x, y, mPaint);
- lx = x;
- ly = y;
- }
-
- // Draw velocity vector.
- mPaint.setARGB(255, 255, 64, 128);
- float xVel = ps.mXVelocity * (1000 / 60);
- float yVel = ps.mYVelocity * (1000 / 60);
- canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
+ if (haveLast) {
+ canvas.drawLine(lastX, lastY, x, y, mPathPaint);
+ canvas.drawPoint(lastX, lastY, mPaint);
+ drawn = true;
}
-
- if (mCurDown && ps.mCurDown) {
- // Draw crosshairs.
- canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
- canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
-
- // Draw current point.
- int pressureLevel = (int)(ps.mCoords.pressure * 255);
- mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
- canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
-
- // Draw current touch ellipse.
- mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
- drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
- ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
-
- // Draw current tool ellipse.
- mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
- drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
- ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
-
- // Draw the orientation arrow.
- float arrowSize = ps.mCoords.toolMajor * 0.7f;
- if (arrowSize < 20) {
- arrowSize = 20;
- }
- mPaint.setARGB(255, pressureLevel, 255, 0);
- float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
- * arrowSize);
- float orientationVectorY = (float) (-Math.cos(ps.mCoords.orientation)
- * arrowSize);
- if (ps.mToolType == MotionEvent.TOOL_TYPE_STYLUS
- || ps.mToolType == MotionEvent.TOOL_TYPE_ERASER) {
- // Show full circle orientation.
- canvas.drawLine(ps.mCoords.x, ps.mCoords.y,
- ps.mCoords.x + orientationVectorX,
- ps.mCoords.y + orientationVectorY,
- mPaint);
- } else {
- // Show half circle orientation.
- canvas.drawLine(
- ps.mCoords.x - orientationVectorX,
- ps.mCoords.y - orientationVectorY,
- ps.mCoords.x + orientationVectorX,
- ps.mCoords.y + orientationVectorY,
- mPaint);
- }
-
- // Draw the tilt point along the orientation arrow.
- float tiltScale = (float) Math.sin(
- ps.mCoords.getAxisValue(MotionEvent.AXIS_TILT));
- canvas.drawCircle(
- ps.mCoords.x + orientationVectorX * tiltScale,
- ps.mCoords.y + orientationVectorY * tiltScale,
- 3.0f, mPaint);
+ lastX = x;
+ lastY = y;
+ haveLast = true;
+ }
+
+ if (drawn) {
+ // Draw movement estimate curve.
+ mPaint.setARGB(128, 128, 0, 128);
+ float lx = ps.mEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
+ float ly = ps.mEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
+ for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {
+ float x = ps.mEstimator.estimateX(i * ESTIMATE_INTERVAL);
+ float y = ps.mEstimator.estimateY(i * ESTIMATE_INTERVAL);
+ canvas.drawLine(lx, ly, x, y, mPaint);
+ lx = x;
+ ly = y;
}
+
+ // Draw velocity vector.
+ mPaint.setARGB(255, 255, 64, 128);
+ float xVel = ps.mXVelocity * (1000 / 60);
+ float yVel = ps.mYVelocity * (1000 / 60);
+ canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
+ }
+
+ if (mCurDown && ps.mCurDown) {
+ // Draw crosshairs.
+ canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
+ canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
+
+ // Draw current point.
+ int pressureLevel = (int)(ps.mCoords.pressure * 255);
+ mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
+ canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
+
+ // Draw current touch ellipse.
+ mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
+ drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
+ ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
+
+ // Draw current tool ellipse.
+ mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
+ drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
+ ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
+
+ // Draw the orientation arrow.
+ float arrowSize = ps.mCoords.toolMajor * 0.7f;
+ if (arrowSize < 20) {
+ arrowSize = 20;
+ }
+ mPaint.setARGB(255, pressureLevel, 255, 0);
+ float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
+ * arrowSize);
+ float orientationVectorY = (float) (-Math.cos(ps.mCoords.orientation)
+ * arrowSize);
+ if (ps.mToolType == MotionEvent.TOOL_TYPE_STYLUS
+ || ps.mToolType == MotionEvent.TOOL_TYPE_ERASER) {
+ // Show full circle orientation.
+ canvas.drawLine(ps.mCoords.x, ps.mCoords.y,
+ ps.mCoords.x + orientationVectorX,
+ ps.mCoords.y + orientationVectorY,
+ mPaint);
+ } else {
+ // Show half circle orientation.
+ canvas.drawLine(
+ ps.mCoords.x - orientationVectorX,
+ ps.mCoords.y - orientationVectorY,
+ ps.mCoords.x + orientationVectorX,
+ ps.mCoords.y + orientationVectorY,
+ mPaint);
+ }
+
+ // Draw the tilt point along the orientation arrow.
+ float tiltScale = (float) Math.sin(
+ ps.mCoords.getAxisValue(MotionEvent.AXIS_TILT));
+ canvas.drawCircle(
+ ps.mCoords.x + orientationVectorX * tiltScale,
+ ps.mCoords.y + orientationVectorY * tiltScale,
+ 3.0f, mPaint);
}
}
}
@@ -461,111 +459,109 @@ public class PointerLocationView extends View {
}
public void addPointerEvent(MotionEvent event) {
- synchronized (mPointers) {
- final int action = event.getAction();
- int NP = mPointers.size();
-
- if (action == MotionEvent.ACTION_DOWN
- || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
- final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
- >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
- if (action == MotionEvent.ACTION_DOWN) {
- for (int p=0; p<NP; p++) {
- final PointerState ps = mPointers.get(p);
- ps.clearTrace();
- ps.mCurDown = false;
- }
- mCurDown = true;
- mCurNumPointers = 0;
- mMaxNumPointers = 0;
- mVelocity.clear();
+ final int action = event.getAction();
+ int NP = mPointers.size();
+
+ if (action == MotionEvent.ACTION_DOWN
+ || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
+ final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
+ if (action == MotionEvent.ACTION_DOWN) {
+ for (int p=0; p<NP; p++) {
+ final PointerState ps = mPointers.get(p);
+ ps.clearTrace();
+ ps.mCurDown = false;
}
+ mCurDown = true;
+ mCurNumPointers = 0;
+ mMaxNumPointers = 0;
+ mVelocity.clear();
+ }
- mCurNumPointers += 1;
- if (mMaxNumPointers < mCurNumPointers) {
- mMaxNumPointers = mCurNumPointers;
- }
+ mCurNumPointers += 1;
+ if (mMaxNumPointers < mCurNumPointers) {
+ mMaxNumPointers = mCurNumPointers;
+ }
- final int id = event.getPointerId(index);
- while (NP <= id) {
- PointerState ps = new PointerState();
- mPointers.add(ps);
- NP++;
- }
-
- if (mActivePointerId < 0 ||
- !mPointers.get(mActivePointerId).mCurDown) {
- mActivePointerId = id;
- }
-
- final PointerState ps = mPointers.get(id);
- ps.mCurDown = true;
+ final int id = event.getPointerId(index);
+ while (NP <= id) {
+ PointerState ps = new PointerState();
+ mPointers.add(ps);
+ NP++;
}
- final int NI = event.getPointerCount();
-
- mVelocity.addMovement(event);
- mVelocity.computeCurrentVelocity(1);
-
- final int N = event.getHistorySize();
- for (int historyPos = 0; historyPos < N; historyPos++) {
- for (int i = 0; i < NI; i++) {
- final int id = event.getPointerId(i);
- final PointerState ps = mCurDown ? mPointers.get(id) : null;
- final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
- event.getHistoricalPointerCoords(i, historyPos, coords);
- if (mPrintCoords) {
- logCoords("Pointer", action, i, coords, id,
- event.getToolType(i), event.getButtonState());
- }
- if (ps != null) {
- ps.addTrace(coords.x, coords.y);
- }
- }
+ if (mActivePointerId < 0 ||
+ !mPointers.get(mActivePointerId).mCurDown) {
+ mActivePointerId = id;
}
+
+ final PointerState ps = mPointers.get(id);
+ ps.mCurDown = true;
+ }
+
+ final int NI = event.getPointerCount();
+
+ mVelocity.addMovement(event);
+ mVelocity.computeCurrentVelocity(1);
+
+ final int N = event.getHistorySize();
+ for (int historyPos = 0; historyPos < N; historyPos++) {
for (int i = 0; i < NI; i++) {
final int id = event.getPointerId(i);
final PointerState ps = mCurDown ? mPointers.get(id) : null;
final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
- event.getPointerCoords(i, coords);
+ event.getHistoricalPointerCoords(i, historyPos, coords);
if (mPrintCoords) {
logCoords("Pointer", action, i, coords, id,
event.getToolType(i), event.getButtonState());
}
if (ps != null) {
ps.addTrace(coords.x, coords.y);
- ps.mXVelocity = mVelocity.getXVelocity(id);
- ps.mYVelocity = mVelocity.getYVelocity(id);
- mVelocity.getEstimator(id, -1, -1, ps.mEstimator);
- ps.mToolType = event.getToolType(i);
}
}
+ }
+ for (int i = 0; i < NI; i++) {
+ final int id = event.getPointerId(i);
+ final PointerState ps = mCurDown ? mPointers.get(id) : null;
+ final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
+ event.getPointerCoords(i, coords);
+ if (mPrintCoords) {
+ logCoords("Pointer", action, i, coords, id,
+ event.getToolType(i), event.getButtonState());
+ }
+ if (ps != null) {
+ ps.addTrace(coords.x, coords.y);
+ ps.mXVelocity = mVelocity.getXVelocity(id);
+ ps.mYVelocity = mVelocity.getYVelocity(id);
+ mVelocity.getEstimator(id, -1, -1, ps.mEstimator);
+ ps.mToolType = event.getToolType(i);
+ }
+ }
+
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
+ final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
+
+ final int id = event.getPointerId(index);
+ final PointerState ps = mPointers.get(id);
+ ps.mCurDown = false;
if (action == MotionEvent.ACTION_UP
- || action == MotionEvent.ACTION_CANCEL
- || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
- final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
- >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
-
- final int id = event.getPointerId(index);
- final PointerState ps = mPointers.get(id);
- ps.mCurDown = false;
-
- if (action == MotionEvent.ACTION_UP
- || action == MotionEvent.ACTION_CANCEL) {
- mCurDown = false;
- mCurNumPointers = 0;
- } else {
- mCurNumPointers -= 1;
- if (mActivePointerId == id) {
- mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
- }
- ps.addTrace(Float.NaN, Float.NaN);
+ || action == MotionEvent.ACTION_CANCEL) {
+ mCurDown = false;
+ mCurNumPointers = 0;
+ } else {
+ mCurNumPointers -= 1;
+ if (mActivePointerId == id) {
+ mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
}
+ ps.addTrace(Float.NaN, Float.NaN);
}
-
- postInvalidate();
}
+
+ invalidate();
}
@Override