diff options
Diffstat (limited to 'core/java')
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("New mail from " + 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("New mail from " + 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 |
