Merge "Removed obsolete opencore related comments"
diff --git a/api/current.txt b/api/current.txt
index d6a17e6..233938b5 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3724,6 +3724,7 @@
method public android.app.Notification.Builder setSmallIcon(int, int);
method public android.app.Notification.Builder setSound(android.net.Uri);
method public android.app.Notification.Builder setSound(android.net.Uri, int);
+ method public android.app.Notification.Builder setSubText(java.lang.CharSequence);
method public android.app.Notification.Builder setTicker(java.lang.CharSequence);
method public android.app.Notification.Builder setTicker(java.lang.CharSequence, android.widget.RemoteViews);
method public android.app.Notification.Builder setUsesIntruderAlert(boolean);
@@ -12734,12 +12735,16 @@
method public void requestPeers(android.net.wifi.p2p.WifiP2pManager.Channel, android.net.wifi.p2p.WifiP2pManager.PeerListListener);
field public static final int BUSY = 2; // 0x2
field public static final int ERROR = 0; // 0x0
+ field public static final java.lang.String EXTRA_DISCOVERY_STATE = "discoveryState";
field public static final java.lang.String EXTRA_NETWORK_INFO = "networkInfo";
field public static final java.lang.String EXTRA_WIFI_P2P_DEVICE = "wifiP2pDevice";
field public static final java.lang.String EXTRA_WIFI_P2P_INFO = "wifiP2pInfo";
field public static final java.lang.String EXTRA_WIFI_STATE = "wifi_p2p_state";
field public static final int P2P_UNSUPPORTED = 1; // 0x1
field public static final java.lang.String WIFI_P2P_CONNECTION_CHANGED_ACTION = "android.net.wifi.p2p.CONNECTION_STATE_CHANGE";
+ field public static final java.lang.String WIFI_P2P_DISCOVERY_CHANGED_ACTION = "android.net.wifi.p2p.DISCOVERY_STATE_CHANGE";
+ field public static final int WIFI_P2P_DISCOVERY_STARTED = 2; // 0x2
+ field public static final int WIFI_P2P_DISCOVERY_STOPPED = 1; // 0x1
field public static final java.lang.String WIFI_P2P_PEERS_CHANGED_ACTION = "android.net.wifi.p2p.PEERS_CHANGED";
field public static final java.lang.String WIFI_P2P_STATE_CHANGED_ACTION = "android.net.wifi.p2p.STATE_CHANGED";
field public static final int WIFI_P2P_STATE_DISABLED = 1; // 0x1
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 @@
// 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/Notification.java b/core/java/android/app/Notification.java
index bbb6a4e..1356801 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -187,7 +187,6 @@
*/
public RemoteViews contentView;
-
/**
* The view that will represent this notification in the pop-up "intruder alert" dialog.
* @hide
@@ -195,6 +194,14 @@
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 +591,9 @@
if (parcel.readInt() != 0) {
intruderView = RemoteViews.CREATOR.createFromParcel(parcel);
}
+ if (parcel.readInt() != 0) {
+ bigContentView = RemoteViews.CREATOR.createFromParcel(parcel);
+ }
}
@Override
@@ -650,6 +660,9 @@
if (this.intruderView != null) {
that.intruderView = this.intruderView.clone();
}
+ if (this.bigContentView != null) {
+ that.bigContentView = this.bigContentView.clone();
+ }
return that;
}
@@ -747,6 +760,13 @@
} else {
parcel.writeInt(0);
}
+
+ if (bigContentView != null) {
+ parcel.writeInt(1);
+ bigContentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
}
/**
@@ -896,6 +916,7 @@
private CharSequence mContentTitle;
private CharSequence mContentText;
private CharSequence mContentInfo;
+ private CharSequence mSubText;
private PendingIntent mContentIntent;
private RemoteViews mContentView;
private PendingIntent mDeleteIntent;
@@ -1013,6 +1034,15 @@
}
/**
+ * 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 +1055,6 @@
/**
* 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 +1066,6 @@
/**
* 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 +1078,6 @@
/**
* Supply a custom RemoteViews to use instead of the platform template.
*
-
* @see Notification#contentView
*/
public Builder setContent(RemoteViews views) {
@@ -1061,17 +1088,12 @@
/**
* 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 +1104,6 @@
/**
* 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 +1136,6 @@
* 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 +1375,28 @@
}
}
- 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 +1418,19 @@
} 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);
@@ -1408,9 +1443,7 @@
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.status_bar_latest_event_content); // no more special large_icon flavor
}
}
@@ -1419,7 +1452,7 @@
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 {
@@ -1516,4 +1549,100 @@
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.applyStandardTemplate(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.applyStandardTemplate(R.layout.status_bar_latest_event_content);
+
+ 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 @@
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 @@
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 Application}.
+ */
+ 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);
+ }
+ }
+ }
+
+ NfcApplicationState findAppState(Application app) {
+ for (NfcApplicationState appState : mApps) {
+ if (appState.app == app) {
+ return appState;
+ }
+ }
+ return null;
+ }
+
+ void registerApplication(Application app) {
+ NfcApplicationState appState = findAppState(app);
+ if (appState == null) {
+ appState = new NfcApplicationState(app);
+ mApps.add(appState);
+ }
+ appState.register();
+ }
+
+ void unregisterApplication(Application app) {
+ NfcApplicationState appState = findAppState(app);
+ if (appState == null) {
+ Log.e(TAG, "app was not registered " + app);
+ return;
+ }
+ appState.unregister();
+ }
/**
* NFC state associated with an {@link Activity}
*/
class NfcActivityState {
- boolean resumed = false; // is the activity resumed
- NdefMessage ndefMessage;
- NfcAdapter.CreateNdefMessageCallback ndefMessageCallback;
- NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback;
+ 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());
+ }
+ public void destroy() {
+ unregisterApplication(activity.getApplication());
+ resumed = false;
+ activity = null;
+ ndefMessage = null;
+ ndefMessageCallback = null;
+ onNdefPushCompleteCallback = null;
+ }
@Override
public String toString() {
- StringBuilder s = new StringBuilder("[").append(resumed).append(" ");
+ StringBuilder s = new StringBuilder("[").append(" ");
s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" ");
s.append(onNdefPushCompleteCallback).append("]");
return s.toString();
}
}
- public NfcActivityManager(NfcAdapter adapter) {
- mAdapter = adapter;
- mNfcState = new WeakHashMap<Activity, NfcActivityState>();
- mDefaultEvent = new NfcEvent(mAdapter);
+ /** find activity state from mActivities */
+ synchronized NfcActivityState findActivityState(Activity activity) {
+ for (NfcActivityState state : mActivities) {
+ if (state.activity == activity) {
+ return state;
+ }
+ }
+ 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);
- }
- }
-
- /**
- * 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);
- }
- }
-
- /**
- * onDestroy hook from fragment attached to 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;
- }
- state.ndefMessage = message;
- if (message == null) {
- maybeRemoveState(activity, state);
- }
- if (state.resumed) {
- updateNfcService(state);
- }
- }
-
- 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;
- }
- state.ndefMessageCallback = callback;
- if (callback == null) {
- maybeRemoveState(activity, state);
- }
- if (state.resumed) {
- updateNfcService(state);
- }
- }
-
- 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;
- }
- state.onNdefPushCompleteCallback = callback;
- if (callback == null) {
- maybeRemoveState(activity, state);
- }
- if (state.resumed) {
- updateNfcService(state);
- }
- }
-
- /**
- * 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);
+ /** 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);
}
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);
+ synchronized NfcActivityState findResumedActivityState() {
+ for (NfcActivityState state : mActivities) {
+ if (state.resumed) {
+ return state;
+ }
+ }
+ return null;
+ }
+
+ synchronized void destroyActivityState(Activity activity) {
+ NfcActivityState activityState = findActivityState(activity);
+ if (activityState != null) {
+ activityState.destroy();
+ mActivities.remove(activityState);
+ }
+ }
+
+ 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 (isResumed) {
+ requestNfcServiceCallback(true);
+ }
+ }
+
+ 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);
+ }
+ }
+
+ 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 @@
}
/**
- * 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(a, message);
+ 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);
+ }
}
}
/**
- * 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(a, callback);
+ 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);
+ }
}
}
/**
- * Set the callback on a successful NDEF push over NFC.
+ * Set a callback on successful Android Beam (TM).
*
- * <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.
+ * <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>One or more activities must be specified.
+ * <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 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(a, callback);
+ 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);
+ }
}
}
@@ -932,4 +1053,12 @@
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 @@
* <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 @@
*
* <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 @@
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 @@
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 @@
// 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 5c97cfa..53ce32d 100644
--- a/core/java/android/service/textservice/SpellCheckerService.java
+++ b/core/java/android/service/textservice/SpellCheckerService.java
@@ -141,8 +141,12 @@
}
/**
- * 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
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index b708750..d0c87c6 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -338,7 +338,7 @@
en = tbend;
if (getSpanStart(spans[i]) < 0) {
- setSpan(false, spans[i],
+ setSpan(true, spans[i],
st - tbstart + start,
en - tbstart + start,
sp.getSpanFlags(spans[i]));
@@ -579,8 +579,7 @@
mSpanEnds[i] = end;
mSpanFlags[i] = flags;
- if (send)
- sendSpanChanged(what, ostart, oend, nstart, nend);
+ if (send) sendSpanChanged(what, ostart, oend, nstart, nend);
return;
}
@@ -610,8 +609,7 @@
mSpanFlags[mSpanCount] = flags;
mSpanCount++;
- if (send)
- sendSpanAdded(what, nstart, nend);
+ if (send) sendSpanAdded(what, nstart, nend);
}
/**
diff --git a/core/java/android/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 @@
}
};
- // 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 @@
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 @@
}
/**
- * 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) {
+ 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, "RemoveAnimationCallbacks: " + action + ", token=" + token);
+ Log.d(TAG, "RemoveCallbacks: type=" + callbackType
+ + ", action=" + action + ", token=" + token);
}
synchronized (mLock) {
- mAnimationCallbackQueue.removeCallbacksLocked(action, token);
+ mCallbackQueues[callbackType].removeCallbacksLocked(action, token);
if (action != null && token == null) {
- mHandler.removeMessages(MSG_DO_SCHEDULE_ANIMATION, action);
+ mHandler.removeMessages(MSG_DO_SCHEDULE_CALLBACK, action);
}
}
}
/**
- * Posts a callback to run on the next draw cycle.
- * The callback only runs once and then is automatically removed.
+ * Tells the choreographer that the application has actually drawn to a surface.
*
- * @param action The callback action to run during the next draw cycle.
- * @param token The callback token, or null if none.
- *
- * @see #removeDrawCallback
+ * 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 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 notifyDrawOccurred() {
if (DEBUG) {
- Log.d(TAG, "PostDrawCallback: " + action + ", token=" + token
- + ", delayMillis=" + delayMillis);
+ Log.d(TAG, "Draw occurred.");
}
- 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);
+ if (USE_PIPELINING) {
+ synchronized (mLock) {
+ if (!mDrewLastFrame) {
+ mDrewLastFrame = true;
+ scheduleFrameLocked(SystemClock.uptimeMillis());
+ }
}
}
}
- /**
- * Removes draw callbacks that have the specified action and token.
- *
- * @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
- */
- public void removeDrawCallbacks(Runnable action, Object token) {
- if (DEBUG) {
- Log.d(TAG, "RemoveDrawCallbacks: " + action + ", token=" + token);
- }
-
- synchronized (mLock) {
- mDrawCallbackQueue.removeCallbacksLocked(action, token);
- if (action != null && token == null) {
- mHandler.removeMessages(MSG_DO_SCHEDULE_DRAW, action);
- }
- }
- }
-
- 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 @@
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 doFrame(int frame) {
+ synchronized (mLock) {
+ if (!mFrameScheduled) {
+ return; // no work to do
}
+ mFrameScheduled = false;
+ mLastFrameTime = SystemClock.uptimeMillis();
+ mDrewLastFrame = false;
+ }
+
+ doCallbacks(Choreographer.CALLBACK_INPUT);
+ doCallbacks(Choreographer.CALLBACK_ANIMATION);
+ doCallbacks(Choreographer.CALLBACK_TRAVERSAL);
+
+ if (DEBUG) {
+ Log.d(TAG, "Frame " + frame + ": Finished, took "
+ + (SystemClock.uptimeMillis() - mLastFrameTime) + " ms.");
}
}
- void doAnimation() {
- doAnimationInner();
-
- if (USE_ANIMATION_TIMER_FOR_DRAW) {
- doDraw();
- }
- }
-
- void doAnimationInner() {
+ void doCallbacks(int callbackType) {
final long start;
Callback callbacks;
synchronized (mLock) {
- if (!mAnimationScheduled) {
- 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 = mCallbackQueues[callbackType].extractDueCallbacksLocked(start);
- callbacks = mAnimationCallbackQueue.extractDueCallbacksLocked(start);
+ if (USE_PIPELINING && callbackType == CALLBACK_TRAVERSAL && mTraversalScheduled) {
+ mTraversalScheduled = false;
+ mHandler.removeMessages(MSG_DO_TRAVERSAL);
+ }
}
if (callbacks != null) {
- runCallbacks(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();
+ }
+
synchronized (mLock) {
- recycleCallbacksLocked(callbacks);
+ do {
+ final Callback next = callbacks.next;
+ recycleCallbackLocked(callbacks);
+ callbacks = next;
+ } while (callbacks != null);
}
}
-
- if (DEBUG) {
- Log.d(TAG, "Animation took " + (SystemClock.uptimeMillis() - start) + " ms.");
- }
- }
-
- void doDraw() {
- 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 = mDrawCallbackQueue.extractDueCallbacksLocked(start);
- }
-
- if (callbacks != null) {
- runCallbacks(callbacks);
- synchronized (mLock) {
- recycleCallbacksLocked(callbacks);
- }
- }
-
- if (DEBUG) {
- Log.d(TAG, "Draw took " + (SystemClock.uptimeMillis() - start) + " ms.");
- }
}
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 @@
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 @@
@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 @@
@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 @@
/**
* 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 @@
setFocusBottomRight(root);
break;
case View.FOCUS_FORWARD:
- if (focused != null && focused.isLayoutRtl()) {
+ if (root.isLayoutRtl()) {
setFocusTopLeft(root);
} else {
setFocusBottomRight(root);
@@ -94,7 +94,7 @@
setFocusTopLeft(root);
break;
case View.FOCUS_BACKWARD:
- if (focused != null && focused.isLayoutRtl()) {
+ if (root.isLayoutRtl()) {
setFocusBottomRight(root);
} else {
setFocusTopLeft(root);
@@ -121,7 +121,7 @@
/**
* 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 @@
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 @@
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 @@
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 @@
/**
- * 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 @@
/**
* 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/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java
index 81c7ebf..be640dc 100644
--- a/core/java/android/view/HardwareRenderer.java
+++ b/core/java/android/view/HardwareRenderer.java
@@ -274,12 +274,13 @@
/**
* 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.
@@ -541,6 +542,7 @@
private boolean mDestroyed;
private final Rect mRedrawClip = new Rect();
+ private final int[] mSurfaceSize = new int[2];
GlRenderer(int glVersion, boolean translucent) {
mGlVersion = glVersion;
@@ -957,17 +959,29 @@
final int surfaceState = checkCurrent();
if (surfaceState != SURFACE_STATE_ERROR) {
+ HardwareCanvas canvas = mCanvas;
+ attachInfo.mHardwareCanvas = canvas;
+
// We had to change the current surface and/or context, redraw everything
if (surfaceState == SURFACE_STATE_UPDATED) {
dirty = null;
+ beginFrame(null);
+ } else {
+ int[] size = mSurfaceSize;
+ beginFrame(size);
+
+ if (size[1] != mHeight || size[0] != mWidth) {
+ mWidth = size[0];
+ mHeight = size[1];
+
+ canvas.setViewport(mWidth, mHeight);
+
+ dirty = null;
+ }
}
- beginFrame();
onPreDraw(dirty);
- HardwareCanvas canvas = mCanvas;
- attachInfo.mHardwareCanvas = canvas;
-
int saveCount = canvas.save();
callbacks.onHardwarePreDraw(canvas);
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 2deeba6..6c964b0 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -1459,7 +1459,7 @@
* 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.
@@ -8983,7 +8983,8 @@
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 +9008,8 @@
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 +9034,8 @@
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);
@@ -12239,8 +12241,9 @@
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 +12259,8 @@
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 +12278,8 @@
*/
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);
}
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index befc1c6..4d589d7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -877,50 +877,6 @@
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,41 +910,67 @@
}
}
- 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");
}
- }
- Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
- try {
- performTraversals();
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
- }
+ 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().");
+ }
+ }
- if (ViewDebug.DEBUG_LATENCY) {
- long now = System.nanoTime();
- Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performTraversals() took "
- + ((now - traversalStartTime) * 0.000001f)
- + "ms.");
- mLastTraversalFinishedTimeNanos = now;
- }
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
+ try {
+ performTraversals();
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
- if (mProfile) {
- Debug.stopMethodTracing();
- mProfile = false;
+ 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;
+ }
}
}
@@ -1937,6 +1919,7 @@
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
+ mChoreographer.notifyDrawOccurred();
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
@@ -2460,7 +2443,7 @@
mInputChannel = null;
}
- unscheduleFrame();
+ unscheduleTraversals();
}
void updateConfiguration(Configuration config, boolean force) {
@@ -3846,6 +3829,7 @@
Message msg = mHandler.obtainMessage(MSG_IME_FINISHED_EVENT);
msg.arg1 = seq;
msg.arg2 = handled ? 1 : 0;
+ msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
@@ -3971,11 +3955,13 @@
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 +4033,46 @@
}
}
- final class FrameRunnable implements Runnable {
- @Override
- public void run() {
- mFrameScheduled = false;
- doFrame();
+ void scheduleConsumeBatchedInput() {
+ if (!mConsumeBatchedInputScheduled) {
+ mConsumeBatchedInputScheduled = true;
+ mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
+ mConsumedBatchedInputRunnable, null);
}
}
- final FrameRunnable mFrameRunnable = new FrameRunnable();
- boolean mFrameScheduled;
+
+ 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() {
+ doTraversal();
+ }
+ }
+ final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
@@ -4069,11 +4086,28 @@
@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 +4143,7 @@
}
if (mPosted && mViews.isEmpty() && mViewRects.isEmpty()) {
- mChoreographer.removeAnimationCallbacks(this, null);
+ mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = false;
}
}
@@ -4150,7 +4184,7 @@
private void postIfNeededLocked() {
if (!mPosted) {
- mChoreographer.postAnimationCallback(this, null);
+ mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = true;
}
}
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 @@
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 @@
}
/**
+ * Enables/disables HTML5 link "prefetch" parameter.
+ */
+ public synchronized void setLinkPrefetchEnabled(boolean flag) {
+ if (mLinkPrefetchEnabled != flag) {
+ mLinkPrefetchEnabled = flag;
+ postSync();
+ }
+ }
+
+ /**
* @see android.webkit.WebSettings#getJavaScriptEnabled()
*/
@Override
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index d225594..422b48d 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -1883,12 +1883,14 @@
@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.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 @@
private boolean mIsAutoFillable;
private boolean mIsAutoCompleteEnabled;
private String mName;
+ private int mBatchLevel;
public WebViewInputConnection() {
super(mWebView, true);
@@ -404,6 +403,24 @@
}
}
+ @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 @@
}
}
- 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 @@
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 @@
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 @@
// 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 @@
}
mAutoFillData = new WebViewCore.AutoFillData();
- mGestureDetector = new GestureDetector(mContext, new TextScrollListener());
mEditTextScroller = new Scroller(context);
}
@@ -1895,11 +1871,17 @@
/* 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
startTouch(x, y, eventTime);
if (mIsEditingText) {
mTouchInEditText = mEditTextBounds.contains(contentX, contentY);
- mGestureDetector.onTouchEvent(ev);
}
break;
}
@@ -6189,13 +6194,6 @@
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 @@
}
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 @@
deltaX = 0;
}
}
- mLastTouchX = x;
- mLastTouchY = y;
-
if (deltaX * deltaX + deltaY * deltaY > mTouchSlopSquare) {
mHeldMotionless = MOTIONLESS_FALSE;
nativeSetIsScrolling(true);
@@ -6339,13 +6335,24 @@
}
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 @@
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 @@
}
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 @@
}
}
- 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 @@
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 @@
}
}
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 4bdb3e2..2a81f08 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,11 +18,8 @@
import android.R;
import android.content.ClipData;
-import android.content.ClipData.Item;
import android.content.ClipboardManager;
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.CompatibilityInfo;
import android.content.res.Resources;
@@ -43,7 +40,6 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
-import android.provider.Settings;
import android.text.BoringLayout;
import android.text.DynamicLayout;
import android.text.Editable;
@@ -57,7 +53,6 @@
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.StaticLayout;
@@ -86,42 +81,31 @@
import android.text.method.WordIterator;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
-import android.text.style.EasyEditSpan;
import android.text.style.ParagraphStyle;
import android.text.style.SpellCheckSpan;
-import android.text.style.SuggestionRangeSpan;
import android.text.style.SuggestionSpan;
-import android.text.style.TextAppearanceSpan;
import android.text.style.URLSpan;
import android.text.style.UpdateAppearance;
import android.text.util.Linkify;
import android.util.AttributeSet;
-import android.util.DisplayMetrics;
import android.util.FloatMath;
import android.util.Log;
import android.util.TypedValue;
import android.view.ActionMode;
-import android.view.ActionMode.Callback;
-import android.view.DisplayList;
import android.view.DragEvent;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
-import android.view.HardwareCanvas;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
-import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewDebug;
-import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
-import android.view.ViewParent;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
-import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -136,10 +120,8 @@
import android.view.inputmethod.InputMethodManager;
import android.view.textservice.SpellCheckerSubtype;
import android.view.textservice.TextServicesManager;
-import android.widget.AdapterView.OnItemClickListener;
import android.widget.RemoteViews.RemoteView;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastMath;
import com.android.internal.widget.EditableInputConnection;
@@ -147,11 +129,7 @@
import java.io.IOException;
import java.lang.ref.WeakReference;
-import java.text.BreakIterator;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.HashMap;
import java.util.Locale;
/**
@@ -267,24 +245,21 @@
private static final int PIXELS = 2;
private static final RectF TEMP_RECTF = new RectF();
- private static final float[] TEMP_POSITION = new float[2];
// XXX should be much larger
private static final int VERY_WIDE = 1024*1024;
- private static final int BLINK = 500;
private static final int ANIMATED_SCROLL_GAP = 250;
private static final InputFilter[] NO_FILTERS = new InputFilter[0];
private static final Spanned EMPTY_SPANNED = new SpannedString("");
- private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
private static final int CHANGE_WATCHER_PRIORITY = 100;
// New state used to change background based on whether this TextView is multiline.
private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
// System wide time for last cut or copy action.
- private static long LAST_CUT_OR_COPY_TIME;
+ static long LAST_CUT_OR_COPY_TIME;
private int mCurrentAlpha = 255;
@@ -316,7 +291,7 @@
mDrawableHeightStart, mDrawableHeightEnd;
int mDrawablePadding;
}
- private Drawables mDrawables;
+ Drawables mDrawables;
private CharWrapper mCharWrapper;
@@ -404,23 +379,23 @@
// It is possible to have a selection even when mEditor is null (programmatically set, like when
// a link is pressed). These highlight-related fields do not go in mEditor.
- private int mHighlightColor = 0x6633B5E5;
+ int mHighlightColor = 0x6633B5E5;
private Path mHighlightPath;
private final Paint mHighlightPaint;
private boolean mHighlightPathBogus = true;
// Although these fields are specific to editable text, they are not added to Editor because
// they are defined by the TextView's style and are theme-dependent.
- private int mCursorDrawableRes;
+ int mCursorDrawableRes;
// These four fields, could be moved to Editor, since we know their default values and we
// could condition the creation of the Editor to a non standard value. This is however
// brittle since the hardcoded values here (such as
// com.android.internal.R.drawable.text_select_handle_left) would have to be updated if the
// default style is modified.
- private int mTextSelectHandleLeftRes;
- private int mTextSelectHandleRightRes;
- private int mTextSelectHandleRes;
- private int mTextEditSuggestionItemLayout;
+ int mTextSelectHandleLeftRes;
+ int mTextSelectHandleRightRes;
+ int mTextSelectHandleRes;
+ int mTextEditSuggestionItemLayout;
/**
* EditText specific data, created on demand when one of the Editor fields is used.
@@ -826,26 +801,20 @@
case com.android.internal.R.styleable.TextView_imeOptions:
createEditorIfNeeded("IME options specified in constructor");
- if (getEditor().mInputContentType == null) {
- getEditor().mInputContentType = new InputContentType();
- }
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.imeOptions = a.getInt(attr,
getEditor().mInputContentType.imeOptions);
break;
case com.android.internal.R.styleable.TextView_imeActionLabel:
createEditorIfNeeded("IME action label specified in constructor");
- if (getEditor().mInputContentType == null) {
- getEditor().mInputContentType = new InputContentType();
- }
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.imeActionLabel = a.getText(attr);
break;
case com.android.internal.R.styleable.TextView_imeActionId:
createEditorIfNeeded("IME action id specified in constructor");
- if (getEditor().mInputContentType == null) {
- getEditor().mInputContentType = new InputContentType();
- }
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.imeActionId = a.getInt(attr,
getEditor().mInputContentType.imeActionId);
break;
@@ -1135,7 +1104,7 @@
setClickable(clickable);
setLongClickable(longClickable);
- prepareCursorControllers();
+ if (mEditor != null) mEditor.prepareCursorControllers();
}
private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
@@ -1216,11 +1185,13 @@
}
// Will change text color
- if (mEditor != null) getEditor().invalidateTextDisplayList();
- prepareCursorControllers();
+ if (mEditor != null) {
+ getEditor().invalidateTextDisplayList();
+ getEditor().prepareCursorControllers();
- // start or stop the cursor blinking as appropriate
- makeBlink();
+ // start or stop the cursor blinking as appropriate
+ getEditor().makeBlink();
+ }
}
/**
@@ -1412,7 +1383,7 @@
fixFocusableAndClickableSettings();
// SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
- prepareCursorControllers();
+ if (mEditor != null) getEditor().prepareCursorControllers();
}
}
@@ -3250,7 +3221,7 @@
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
- prepareCursorControllers();
+ if (mEditor != null) getEditor().prepareCursorControllers();
}
/**
@@ -3366,12 +3337,37 @@
return mHint;
}
+ boolean isSingleLine() {
+ return mSingleLine;
+ }
+
private static boolean isMultilineInputType(int type) {
return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
(EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
}
/**
+ * Removes the suggestion spans.
+ */
+ CharSequence removeSuggestionSpans(CharSequence text) {
+ if (text instanceof Spanned) {
+ Spannable spannable;
+ if (text instanceof Spannable) {
+ spannable = (Spannable) text;
+ } else {
+ spannable = new SpannableString(text);
+ text = spannable;
+ }
+
+ SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
+ for (int i = 0; i < spans.length; i++) {
+ spannable.removeSpan(spans[i]);
+ }
+ }
+ return text;
+ }
+
+ /**
* Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
* will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
* to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
@@ -3543,9 +3539,7 @@
*/
public void setImeOptions(int imeOptions) {
createEditorIfNeeded("IME options specified");
- if (getEditor().mInputContentType == null) {
- getEditor().mInputContentType = new InputContentType();
- }
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.imeOptions = imeOptions;
}
@@ -3572,9 +3566,7 @@
*/
public void setImeActionLabel(CharSequence label, int actionId) {
createEditorIfNeeded("IME action label specified");
- if (getEditor().mInputContentType == null) {
- getEditor().mInputContentType = new InputContentType();
- }
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.imeActionLabel = label;
getEditor().mInputContentType.imeActionId = actionId;
}
@@ -3611,9 +3603,7 @@
*/
public void setOnEditorActionListener(OnEditorActionListener l) {
createEditorIfNeeded("Editor action listener set");
- if (getEditor().mInputContentType == null) {
- getEditor().mInputContentType = new InputContentType();
- }
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.onEditorActionListener = l;
}
@@ -3638,7 +3628,7 @@
* @see #setOnEditorActionListener
*/
public void onEditorAction(int actionCode) {
- final InputContentType ict = mEditor == null ? null : getEditor().mInputContentType;
+ final Editor.InputContentType ict = mEditor == null ? null : getEditor().mInputContentType;
if (ict != null) {
if (ict.onEditorActionListener != null) {
if (ict.onEditorActionListener.onEditorAction(this,
@@ -3710,8 +3700,7 @@
*/
public void setPrivateImeOptions(String type) {
createEditorIfNeeded("Private IME option set");
- if (getEditor().mInputContentType == null)
- getEditor().mInputContentType = new InputContentType();
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.privateImeOptions = type;
}
@@ -3740,8 +3729,7 @@
public void setInputExtras(int xmlResId) throws XmlPullParserException, IOException {
createEditorIfNeeded("Input extra set");
XmlResourceParser parser = getResources().getXml(xmlResId);
- if (getEditor().mInputContentType == null)
- getEditor().mInputContentType = new InputContentType();
+ getEditor().createInputContentTypeIfNeeded();
getEditor().mInputContentType.extras = new Bundle();
getResources().parseBundleExtras(parser, getEditor().mInputContentType.extras);
}
@@ -3761,7 +3749,7 @@
createEditorIfNeeded("get Input extra");
if (getEditor().mInputContentType == null) {
if (!create) return null;
- getEditor().mInputContentType = new InputContentType();
+ getEditor().createInputContentTypeIfNeeded();
}
if (getEditor().mInputContentType.extras == null) {
if (!create) return null;
@@ -3811,142 +3799,9 @@
*/
public void setError(CharSequence error, Drawable icon) {
createEditorIfNeeded("setError");
- error = TextUtils.stringOrSpannedString(error);
-
- getEditor().mError = error;
- getEditor().mErrorWasChanged = true;
- final Drawables dr = mDrawables;
- if (dr != null) {
- switch (getResolvedLayoutDirection()) {
- default:
- case LAYOUT_DIRECTION_LTR:
- setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
- dr.mDrawableBottom);
- break;
- case LAYOUT_DIRECTION_RTL:
- setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
- dr.mDrawableBottom);
- break;
- }
- } else {
- setCompoundDrawables(null, null, icon, null);
- }
-
- if (error == null) {
- if (getEditor().mErrorPopup != null) {
- if (getEditor().mErrorPopup.isShowing()) {
- getEditor().mErrorPopup.dismiss();
- }
-
- getEditor().mErrorPopup = null;
- }
- } else {
- if (isFocused()) {
- showError();
- }
- }
+ getEditor().setError(error, icon);
}
- private void showError() {
- if (getWindowToken() == null) {
- getEditor().mShowErrorAfterAttach = true;
- return;
- }
-
- if (getEditor().mErrorPopup == null) {
- LayoutInflater inflater = LayoutInflater.from(getContext());
- final TextView err = (TextView) inflater.inflate(
- com.android.internal.R.layout.textview_hint, null);
-
- final float scale = getResources().getDisplayMetrics().density;
- getEditor().mErrorPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
- getEditor().mErrorPopup.setFocusable(false);
- // The user is entering text, so the input method is needed. We
- // don't want the popup to be displayed on top of it.
- getEditor().mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
- }
-
- TextView tv = (TextView) getEditor().mErrorPopup.getContentView();
- chooseSize(getEditor().mErrorPopup, getEditor().mError, tv);
- tv.setText(getEditor().mError);
-
- getEditor().mErrorPopup.showAsDropDown(this, getErrorX(), getErrorY());
- getEditor().mErrorPopup.fixDirection(getEditor().mErrorPopup.isAboveAnchor());
- }
-
- /**
- * Returns the Y offset to make the pointy top of the error point
- * at the middle of the error icon.
- */
- private int getErrorX() {
- /*
- * The "25" is the distance between the point and the right edge
- * of the background
- */
- final float scale = getResources().getDisplayMetrics().density;
-
- final Drawables dr = mDrawables;
- return getWidth() - getEditor().mErrorPopup.getWidth() - getPaddingRight() -
- (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
- }
-
- /**
- * Returns the Y offset to make the pointy top of the error point
- * at the bottom of the error icon.
- */
- private int getErrorY() {
- /*
- * Compound, not extended, because the icon is not clipped
- * if the text height is smaller.
- */
- final int compoundPaddingTop = getCompoundPaddingTop();
- int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
-
- final Drawables dr = mDrawables;
- int icontop = compoundPaddingTop +
- (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
-
- /*
- * The "2" is the distance between the point and the top edge
- * of the background.
- */
- final float scale = getResources().getDisplayMetrics().density;
- return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
- (int) (2 * scale + 0.5f);
- }
-
- private void hideError() {
- if (getEditor().mErrorPopup != null) {
- if (getEditor().mErrorPopup.isShowing()) {
- getEditor().mErrorPopup.dismiss();
- }
- }
-
- getEditor().mShowErrorAfterAttach = false;
- }
-
- private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
- int wid = tv.getPaddingLeft() + tv.getPaddingRight();
- int ht = tv.getPaddingTop() + tv.getPaddingBottom();
-
- int defaultWidthInPixels = getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.textview_error_popup_default_width);
- Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
- Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
- float max = 0;
- for (int i = 0; i < l.getLineCount(); i++) {
- max = Math.max(max, l.getLineWidth(i));
- }
-
- /*
- * Now set the popup size to be big enough for the text plus the border capped
- * to DEFAULT_MAX_POPUP_WIDTH
- */
- pop.setWidth(wid + (int) Math.ceil(max));
- pop.setHeight(ht + l.getHeight());
- }
-
-
@Override
protected boolean setFrame(int l, int t, int r, int b) {
boolean result = super.setFrame(l, t, r, b);
@@ -4009,7 +3864,7 @@
/////////////////////////////////////////////////////////////////////////
- private int getVerticalOffset(boolean forceNormal) {
+ int getVerticalOffset(boolean forceNormal) {
int voffset = 0;
final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
@@ -4071,7 +3926,7 @@
return voffset;
}
- private void invalidateCursorPath() {
+ void invalidateCursorPath() {
if (mHighlightPathBogus) {
invalidateCursor();
} else {
@@ -4114,7 +3969,7 @@
}
}
- private void invalidateCursor() {
+ void invalidateCursor() {
int where = getSelectionEnd();
invalidateCursor(where, where, where);
@@ -4130,8 +3985,6 @@
/**
* Invalidates the region of text enclosed between the start and end text offsets.
- *
- * @hide
*/
void invalidateRegion(int start, int end, boolean invalidateCursor) {
if (mLayout == null) {
@@ -4237,15 +4090,15 @@
// - onFocusChanged cannot start it when focus is given to a view with selected text (after
// a screen rotation) since layout is not yet initialized at that point.
if (mEditor != null && getEditor().mCreatedWithASelection) {
- startSelectionActionMode();
+ getEditor().startSelectionActionMode();
getEditor().mCreatedWithASelection = false;
}
// Phone specific code (there is no ExtractEditText on tablets).
// ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
// not be set. Do the test here instead.
- if (this instanceof ExtractEditText && hasSelection()) {
- startSelectionActionMode();
+ if (this instanceof ExtractEditText && hasSelection() && mEditor != null) {
+ getEditor().startSelectionActionMode();
}
getViewTreeObserver().removeOnPreDrawListener(this);
@@ -4260,11 +4113,6 @@
mTemporaryDetach = false;
- if (mEditor != null && getEditor().mShowErrorAfterAttach) {
- showError();
- getEditor().mShowErrorAfterAttach = false;
- }
-
// Resolve drawables as the layout direction has been resolved
resolveDrawables();
@@ -4495,7 +4343,7 @@
setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
// Called by setText above, but safer in case of future code changes
- prepareCursorControllers();
+ getEditor().prepareCursorControllers();
}
@Override
@@ -4536,8 +4384,9 @@
final int selEnd = getSelectionEnd();
if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) {
if (selStart == selEnd) {
- if (mEditor != null && isCursorVisible() &&
- (SystemClock.uptimeMillis() - getEditor().mShowCursor) % (2 * BLINK) < BLINK) {
+ if (mEditor != null && getEditor().isCursorVisible() &&
+ (SystemClock.uptimeMillis() - getEditor().mShowCursor) %
+ (2 * Editor.BLINK) < Editor.BLINK) {
if (mHighlightPathBogus) {
if (mHighlightPath == null) mHighlightPath = new Path();
mHighlightPath.reset();
@@ -4730,14 +4579,14 @@
Path highlight = getUpdatedHighlightPath();
if (mEditor != null) {
- getEditor().onDraw(canvas, layout, highlight, cursorOffsetVertical);
+ getEditor().onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
+ }
- if (mMarquee != null && mMarquee.shouldDrawGhost()) {
- canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
- layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
- }
+ if (mMarquee != null && mMarquee.shouldDrawGhost()) {
+ canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
+ layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
canvas.restore();
@@ -4853,7 +4702,6 @@
/**
* @hide
- * @param offsetRequired
*/
@Override
protected int getFadeTop(boolean offsetRequired) {
@@ -4871,7 +4719,6 @@
/**
* @hide
- * @param offsetRequired
*/
@Override
protected int getFadeHeight(boolean offsetRequired) {
@@ -5252,9 +5099,7 @@
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (onCheckIsTextEditor() && isEnabled()) {
- if (getEditor().mInputMethodState == null) {
- getEditor().mInputMethodState = new InputMethodState();
- }
+ getEditor().createInputMethodStateIfNeeded();
outAttrs.inputType = getInputType();
if (getEditor().mInputContentType != null) {
outAttrs.imeOptions = getEditor().mInputContentType.imeOptions;
@@ -5307,122 +5152,11 @@
* based on the information in <var>request</var> in to <var>outText</var>.
* @return Returns true if the text was successfully extracted, else false.
*/
- public boolean extractText(ExtractedTextRequest request,
- ExtractedText outText) {
- return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
- EXTRACT_UNKNOWN, outText);
+ public boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+ createEditorIfNeeded("extractText");
+ return getEditor().extractText(request, outText);
}
-
- static final int EXTRACT_NOTHING = -2;
- static final int EXTRACT_UNKNOWN = -1;
-
- boolean extractTextInternal(ExtractedTextRequest request,
- int partialStartOffset, int partialEndOffset, int delta,
- ExtractedText outText) {
- final CharSequence content = mText;
- if (content != null) {
- if (partialStartOffset != EXTRACT_NOTHING) {
- final int N = content.length();
- if (partialStartOffset < 0) {
- outText.partialStartOffset = outText.partialEndOffset = -1;
- partialStartOffset = 0;
- partialEndOffset = N;
- } else {
- // Now use the delta to determine the actual amount of text
- // we need.
- partialEndOffset += delta;
- // Adjust offsets to ensure we contain full spans.
- if (content instanceof Spanned) {
- Spanned spanned = (Spanned)content;
- Object[] spans = spanned.getSpans(partialStartOffset,
- partialEndOffset, ParcelableSpan.class);
- int i = spans.length;
- while (i > 0) {
- i--;
- int j = spanned.getSpanStart(spans[i]);
- if (j < partialStartOffset) partialStartOffset = j;
- j = spanned.getSpanEnd(spans[i]);
- if (j > partialEndOffset) partialEndOffset = j;
- }
- }
- outText.partialStartOffset = partialStartOffset;
- outText.partialEndOffset = partialEndOffset - delta;
- if (partialStartOffset > N) {
- partialStartOffset = N;
- } else if (partialStartOffset < 0) {
- partialStartOffset = 0;
- }
- if (partialEndOffset > N) {
- partialEndOffset = N;
- } else if (partialEndOffset < 0) {
- partialEndOffset = 0;
- }
- }
- if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
- outText.text = content.subSequence(partialStartOffset,
- partialEndOffset);
- } else {
- outText.text = TextUtils.substring(content, partialStartOffset,
- partialEndOffset);
- }
- } else {
- outText.partialStartOffset = 0;
- outText.partialEndOffset = 0;
- outText.text = "";
- }
- outText.flags = 0;
- if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
- outText.flags |= ExtractedText.FLAG_SELECTING;
- }
- if (mSingleLine) {
- outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
- }
- outText.startOffset = 0;
- outText.selectionStart = getSelectionStart();
- outText.selectionEnd = getSelectionEnd();
- return true;
- }
- return false;
- }
-
- boolean reportExtractedText() {
- final InputMethodState ims = getEditor().mInputMethodState;
- if (ims != null) {
- final boolean contentChanged = ims.mContentChanged;
- if (contentChanged || ims.mSelectionModeChanged) {
- ims.mContentChanged = false;
- ims.mSelectionModeChanged = false;
- final ExtractedTextRequest req = ims.mExtracting;
- if (req != null) {
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) {
- if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
- + ims.mChangedStart + " end=" + ims.mChangedEnd
- + " delta=" + ims.mChangedDelta);
- if (ims.mChangedStart < 0 && !contentChanged) {
- ims.mChangedStart = EXTRACT_NOTHING;
- }
- if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
- ims.mChangedDelta, ims.mTmpExtracted)) {
- if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
- + ims.mTmpExtracted.partialStartOffset
- + " end=" + ims.mTmpExtracted.partialEndOffset
- + ": " + ims.mTmpExtracted.text);
- imm.updateExtractedText(this, req.token, ims.mTmpExtracted);
- ims.mChangedStart = EXTRACT_UNKNOWN;
- ims.mChangedEnd = EXTRACT_UNKNOWN;
- ims.mChangedDelta = 0;
- ims.mContentChanged = false;
- return true;
- }
- }
- }
- }
- }
- return false;
- }
-
/**
* This is used to remove all style-impacting spans from text before new
* extracted text is being replaced into it, so that we don't have any
@@ -5436,7 +5170,7 @@
spannable.removeSpan(spans[i]);
}
}
-
+
/**
* Apply to this text view the given extracted text, as previously
* returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
@@ -5492,7 +5226,7 @@
// This would stop a possible selection mode, but no such mode is started in case
// extracted mode will start. Some text is selected though, and will trigger an action mode
// in the extracted view.
- hideControllers();
+ getEditor().hideControllers();
}
/**
@@ -5518,87 +5252,15 @@
* @param info The auto correct info about the text that was corrected.
*/
public void onCommitCorrection(CorrectionInfo info) {
- if (mEditor == null) return;
- if (getEditor().mCorrectionHighlighter == null) {
- getEditor().mCorrectionHighlighter = new CorrectionHighlighter();
- } else {
- getEditor().mCorrectionHighlighter.invalidate(false);
- }
-
- getEditor().mCorrectionHighlighter.highlight(info);
+ if (mEditor != null) getEditor().onCommitCorrection(info);
}
public void beginBatchEdit() {
- if (mEditor == null) return;
- getEditor().mInBatchEditControllers = true;
- final InputMethodState ims = getEditor().mInputMethodState;
- if (ims != null) {
- int nesting = ++ims.mBatchEditNesting;
- if (nesting == 1) {
- ims.mCursorChanged = false;
- ims.mChangedDelta = 0;
- if (ims.mContentChanged) {
- // We already have a pending change from somewhere else,
- // so turn this into a full update.
- ims.mChangedStart = 0;
- ims.mChangedEnd = mText.length();
- } else {
- ims.mChangedStart = EXTRACT_UNKNOWN;
- ims.mChangedEnd = EXTRACT_UNKNOWN;
- ims.mContentChanged = false;
- }
- onBeginBatchEdit();
- }
- }
+ if (mEditor != null) getEditor().beginBatchEdit();
}
public void endBatchEdit() {
- if (mEditor == null) return;
- getEditor().mInBatchEditControllers = false;
- final InputMethodState ims = getEditor().mInputMethodState;
- if (ims != null) {
- int nesting = --ims.mBatchEditNesting;
- if (nesting == 0) {
- finishBatchEdit(ims);
- }
- }
- }
-
- void ensureEndedBatchEdit() {
- final InputMethodState ims = getEditor().mInputMethodState;
- if (ims != null && ims.mBatchEditNesting != 0) {
- ims.mBatchEditNesting = 0;
- finishBatchEdit(ims);
- }
- }
-
- void finishBatchEdit(final InputMethodState ims) {
- onEndBatchEdit();
-
- if (ims.mContentChanged || ims.mSelectionModeChanged) {
- updateAfterEdit();
- reportExtractedText();
- } else if (ims.mCursorChanged) {
- // Cheezy way to get us to report the current cursor location.
- invalidateCursor();
- }
- }
-
- void updateAfterEdit() {
- invalidate();
- int curs = getSelectionStart();
-
- if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
- registerForPreDraw();
- }
-
- if (curs >= 0) {
- mHighlightPathBogus = true;
- makeBlink();
- bringPointIntoView(curs);
- }
-
- checkForResize();
+ if (mEditor != null) getEditor().endBatchEdit();
}
/**
@@ -5644,7 +5306,7 @@
mBoring = mHintBoring = null;
// Since it depends on the value of mLayout
- prepareCursorControllers();
+ if (mEditor != null) getEditor().prepareCursorControllers();
}
/**
@@ -5826,7 +5488,7 @@
}
// CursorControllers need a non-null mLayout
- prepareCursorControllers();
+ if (mEditor != null) getEditor().prepareCursorControllers();
}
private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
@@ -6638,11 +6300,11 @@
r.bottom += verticalOffset;
}
- private int viewportToContentHorizontalOffset() {
+ int viewportToContentHorizontalOffset() {
return getCompoundPaddingLeft() - mScrollX;
}
- private int viewportToContentVerticalOffset() {
+ int viewportToContentVerticalOffset() {
int offset = getExtendedPaddingTop() - mScrollY;
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
offset += getVerticalOffset(false);
@@ -6855,18 +6517,13 @@
getEditor().mCursorVisible = visible;
invalidate();
- makeBlink();
+ getEditor().makeBlink();
// InsertionPointCursorController depends on mCursorVisible
- prepareCursorControllers();
+ getEditor().prepareCursorControllers();
}
}
- private boolean isCursorVisible() {
- // The default value is true, even when there is no associated Editor
- return mEditor == null ? true : (getEditor().mCursorVisible && isTextEditable());
- }
-
private boolean canMarquee() {
int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
return width > 0 && (mLayout.getLineWidth(0) > width ||
@@ -7049,12 +6706,29 @@
}
}
+ void updateAfterEdit() {
+ invalidate();
+ int curs = getSelectionStart();
+
+ if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ registerForPreDraw();
+ }
+
+ if (curs >= 0) {
+ mHighlightPathBogus = true;
+ if (mEditor != null) getEditor().makeBlink();
+ bringPointIntoView(curs);
+ }
+
+ checkForResize();
+ }
+
/**
* Not private so it can be called from an inner class without going
* through a thunk.
*/
void handleTextChanged(CharSequence buffer, int start, int before, int after) {
- final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
+ final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
if (ims == null || ims.mBatchEditNesting == 0) {
updateAfterEdit();
}
@@ -7085,7 +6759,7 @@
boolean selChanged = false;
int newSelStart=-1, newSelEnd=-1;
- final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
+ final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
if (what == Selection.SELECTION_END) {
selChanged = true;
@@ -7094,7 +6768,7 @@
if (oldStart >= 0 || newStart >= 0) {
invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
registerForPreDraw();
- makeBlink();
+ if (mEditor != null) getEditor().makeBlink();
}
}
@@ -7186,20 +6860,6 @@
}
/**
- * Create new SpellCheckSpans on the modified region.
- */
- private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
- if (isTextEditable() && isSuggestionsEnabled() && !(this instanceof ExtractEditText)) {
- if (getEditor().mSpellChecker == null && createSpellChecker) {
- getEditor().mSpellChecker = new SpellChecker(this);
- }
- if (getEditor().mSpellChecker != null) {
- getEditor().mSpellChecker.spellCheck(start, end);
- }
- }
- }
-
- /**
* @hide
*/
@Override
@@ -7219,7 +6879,7 @@
// Because of View recycling in ListView, there is no easy way to know when a TextView with
// selection becomes visible again. Until a better solution is found, stop text selection
// mode (if any) as soon as this TextView is recycled.
- if (mEditor != null) hideControllers();
+ if (mEditor != null) getEditor().hideControllers();
}
@Override
@@ -7269,7 +6929,7 @@
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (mEditor != null && visibility != VISIBLE) {
- hideControllers();
+ getEditor().hideControllers();
}
}
@@ -7350,31 +7010,8 @@
handled |= imm != null && imm.showSoftInput(this, 0);
}
- boolean selectAllGotFocus = getEditor().mSelectAllOnFocus && didTouchFocusSelect();
- hideControllers();
- if (!selectAllGotFocus && mText.length() > 0) {
- // Move cursor
- final int offset = getOffsetForPosition(event.getX(), event.getY());
- Selection.setSelection((Spannable) mText, offset);
- if (getEditor().mSpellChecker != null) {
- // When the cursor moves, the word that was typed may need spell check
- getEditor().mSpellChecker.onSelectionChanged();
- }
- if (!extractedTextModeWillBeStarted()) {
- if (isCursorInsideEasyCorrectionSpan()) {
- getEditor().mShowSuggestionRunnable = new Runnable() {
- public void run() {
- showSuggestions();
- }
- };
- // removeCallbacks is performed on every touch
- postDelayed(getEditor().mShowSuggestionRunnable,
- ViewConfiguration.getDoubleTapTimeout());
- } else if (hasInsertionController()) {
- getInsertionController().show();
- }
- }
- }
+ // The above condition ensures that the mEditor is not null
+ getEditor().onTouchUpEvent(event);
handled = true;
}
@@ -7387,53 +7024,6 @@
return superResult;
}
- /**
- * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
- */
- private boolean isCursorInsideSuggestionSpan() {
- if (!(mText instanceof Spannable)) return false;
-
- SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
- getSelectionEnd(), SuggestionSpan.class);
- return (suggestionSpans.length > 0);
- }
-
- /**
- * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
- * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
- */
- private boolean isCursorInsideEasyCorrectionSpan() {
- Spannable spannable = (Spannable) mText;
- SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
- getSelectionEnd(), SuggestionSpan.class);
- for (int i = 0; i < suggestionSpans.length; i++) {
- if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Downgrades to simple suggestions all the easy correction spans that are not a spell check
- * span.
- */
- private void downgradeEasyCorrectionSpans() {
- if (mText instanceof Spannable) {
- Spannable spannable = (Spannable) mText;
- SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
- spannable.length(), SuggestionSpan.class);
- for (int i = 0; i < suggestionSpans.length; i++) {
- int flags = suggestionSpans[i].getFlags();
- if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
- && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
- flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
- suggestionSpans[i].setFlags(flags);
- }
- }
- }
- }
-
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (mMovement != null && mText instanceof Spannable && mLayout != null) {
@@ -7450,44 +7040,11 @@
return super.onGenericMotionEvent(event);
}
- private void prepareCursorControllers() {
- if (mEditor == null) return;
-
- boolean windowSupportsHandles = false;
-
- ViewGroup.LayoutParams params = getRootView().getLayoutParams();
- if (params instanceof WindowManager.LayoutParams) {
- WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
- windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
- || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
- }
-
- getEditor().mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
- getEditor().mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
- mLayout != null;
-
- if (!getEditor().mInsertionControllerEnabled) {
- hideInsertionPointCursorController();
- if (getEditor().mInsertionPointCursorController != null) {
- getEditor().mInsertionPointCursorController.onDetached();
- getEditor().mInsertionPointCursorController = null;
- }
- }
-
- if (!getEditor().mSelectionControllerEnabled) {
- stopSelectionActionMode();
- if (getEditor().mSelectionModifierCursorController != null) {
- getEditor().mSelectionModifierCursorController.onDetached();
- getEditor().mSelectionModifierCursorController = null;
- }
- }
- }
-
/**
* @return True iff this TextView contains a text that can be edited, or if this is
* a selectable TextView.
*/
- private boolean isTextEditable() {
+ boolean isTextEditable() {
return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
}
@@ -7522,32 +7079,6 @@
mScroller = s;
}
- /**
- * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
- */
- private boolean shouldBlink() {
- if (mEditor == null || !isCursorVisible() || !isFocused()) return false;
-
- final int start = getSelectionStart();
- if (start < 0) return false;
-
- final int end = getSelectionEnd();
- if (end < 0) return false;
-
- return start == end;
- }
-
- private void makeBlink() {
- if (shouldBlink()) {
- getEditor().mShowCursor = SystemClock.uptimeMillis();
- if (getEditor().mBlink == null) getEditor().mBlink = new Blink(this);
- getEditor().mBlink.removeCallbacks(getEditor().mBlink);
- getEditor().mBlink.postAtTime(getEditor().mBlink, getEditor().mShowCursor + BLINK);
- } else {
- if (mEditor != null && getEditor().mBlink != null) getEditor().mBlink.removeCallbacks(getEditor().mBlink);
- }
- }
-
@Override
protected float getLeftFadingEdgeStrength() {
if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
@@ -7726,10 +7257,10 @@
/**
* Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
* TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
- * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
+ * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not sufficient.
*/
private boolean canSelectText() {
- return hasSelectionController() && mText.length() != 0;
+ return mText.length() != 0 && mEditor != null && getEditor().hasSelectionController();
}
/**
@@ -7738,7 +7269,7 @@
*
* See also {@link #canSelectText()}.
*/
- private boolean textCanBeSelected() {
+ boolean textCanBeSelected() {
// prepareCursorController() relies on this method.
// If you change this condition, make sure prepareCursorController is called anywhere
// the value of this condition might be changed.
@@ -7746,112 +7277,6 @@
return isTextEditable() || (isTextSelectable() && mText instanceof Spannable && isEnabled());
}
- private boolean canCut() {
- if (hasPasswordTransformationMethod()) {
- return false;
- }
-
- if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) {
- return true;
- }
-
- return false;
- }
-
- private boolean canCopy() {
- if (hasPasswordTransformationMethod()) {
- return false;
- }
-
- if (mText.length() > 0 && hasSelection()) {
- return true;
- }
-
- return false;
- }
-
- private boolean canPaste() {
- return (mText instanceof Editable &&
- mEditor != null && getEditor().mKeyListener != null &&
- getSelectionStart() >= 0 &&
- getSelectionEnd() >= 0 &&
- ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
- hasPrimaryClip());
- }
-
- private boolean selectAll() {
- final int length = mText.length();
- Selection.setSelection((Spannable) mText, 0, length);
- return length > 0;
- }
-
- /**
- * Adjusts selection to the word under last touch offset.
- * Return true if the operation was successfully performed.
- */
- private boolean selectCurrentWord() {
- if (!canSelectText()) {
- return false;
- }
-
- if (hasPasswordTransformationMethod()) {
- // Always select all on a password field.
- // Cut/copy menu entries are not available for passwords, but being able to select all
- // is however useful to delete or paste to replace the entire content.
- return selectAll();
- }
-
- int inputType = getInputType();
- int klass = inputType & InputType.TYPE_MASK_CLASS;
- int variation = inputType & InputType.TYPE_MASK_VARIATION;
-
- // Specific text field types: select the entire text for these
- if (klass == InputType.TYPE_CLASS_NUMBER ||
- klass == InputType.TYPE_CLASS_PHONE ||
- klass == InputType.TYPE_CLASS_DATETIME ||
- variation == InputType.TYPE_TEXT_VARIATION_URI ||
- variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
- variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
- variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
- return selectAll();
- }
-
- long lastTouchOffsets = getLastTouchOffsets();
- final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
- final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
-
- // Safety check in case standard touch event handling has been bypassed
- if (minOffset < 0 || minOffset >= mText.length()) return false;
- if (maxOffset < 0 || maxOffset >= mText.length()) return false;
-
- int selectionStart, selectionEnd;
-
- // If a URLSpan (web address, email, phone...) is found at that position, select it.
- URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
- if (urlSpans.length >= 1) {
- URLSpan urlSpan = urlSpans[0];
- selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
- selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
- } else {
- final WordIterator wordIterator = getWordIterator();
- wordIterator.setCharSequence(mText, minOffset, maxOffset);
-
- selectionStart = wordIterator.getBeginning(minOffset);
- selectionEnd = wordIterator.getEnd(maxOffset);
-
- if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
- selectionStart == selectionEnd) {
- // Possible when the word iterator does not properly handle the text's language
- long range = getCharRange(minOffset);
- selectionStart = TextUtils.unpackRangeStartFromLong(range);
- selectionEnd = TextUtils.unpackRangeEndFromLong(range);
- }
- }
-
- Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
- return selectionEnd > selectionStart;
- }
-
/**
* This is a temporary method. Future versions may support multi-locale text.
*
@@ -7877,45 +7302,16 @@
}
/**
+ * This method is used by the ArrowKeyMovementMethod to jump from one word to the other.
+ * Made available to achieve a consistent behavior.
* @hide
*/
public WordIterator getWordIterator() {
- if (getEditor().mWordIterator == null) {
- getEditor().mWordIterator = new WordIterator(getTextServicesLocale());
+ if (getEditor() != null) {
+ return mEditor.getWordIterator();
+ } else {
+ return null;
}
- return getEditor().mWordIterator;
- }
-
- private long getCharRange(int offset) {
- final int textLength = mText.length();
- if (offset + 1 < textLength) {
- final char currentChar = mText.charAt(offset);
- final char nextChar = mText.charAt(offset + 1);
- if (Character.isSurrogatePair(currentChar, nextChar)) {
- return TextUtils.packRangeInLong(offset, offset + 2);
- }
- }
- if (offset < textLength) {
- return TextUtils.packRangeInLong(offset, offset + 1);
- }
- if (offset - 2 >= 0) {
- final char previousChar = mText.charAt(offset - 1);
- final char previousPreviousChar = mText.charAt(offset - 2);
- if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
- return TextUtils.packRangeInLong(offset - 2, offset);
- }
- }
- if (offset - 1 >= 0) {
- return TextUtils.packRangeInLong(offset - 1, offset);
- }
- return TextUtils.packRangeInLong(offset, offset);
- }
-
- private long getLastTouchOffsets() {
- SelectionModifierCursorController selectionController = getSelectionController();
- final int minOffset = selectionController.getMinTouchOffset();
- final int maxOffset = selectionController.getMaxTouchOffset();
- return TextUtils.packRangeInLong(minOffset, maxOffset);
}
@Override
@@ -8004,11 +7400,10 @@
return imm != null && imm.isActive(this);
}
- // Selection context mode
- private static final int ID_SELECT_ALL = android.R.id.selectAll;
- private static final int ID_CUT = android.R.id.cut;
- private static final int ID_COPY = android.R.id.copy;
- private static final int ID_PASTE = android.R.id.paste;
+ static final int ID_SELECT_ALL = android.R.id.selectAll;
+ static final int ID_CUT = android.R.id.cut;
+ static final int ID_COPY = android.R.id.copy;
+ static final int ID_PASTE = android.R.id.paste;
/**
* Called when a context menu option for the text view is selected. Currently
@@ -8033,7 +7428,7 @@
case ID_SELECT_ALL:
// This does not enter text selection mode. Text is highlighted, so that it can be
// bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
- selectAll();
+ selectAllText();
return true;
case ID_PASTE:
@@ -8054,89 +7449,10 @@
return false;
}
- private CharSequence getTransformedText(int start, int end) {
+ CharSequence getTransformedText(int start, int end) {
return removeSuggestionSpans(mTransformed.subSequence(start, end));
}
- /**
- * Prepare text so that there are not zero or two spaces at beginning and end of region defined
- * by [min, max] when replacing this region by paste.
- * Note that if there were two spaces (or more) at that position before, they are kept. We just
- * make sure we do not add an extra one from the paste content.
- */
- private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
- if (paste.length() > 0) {
- if (min > 0) {
- final char charBefore = mTransformed.charAt(min - 1);
- final char charAfter = paste.charAt(0);
-
- if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
- // Two spaces at beginning of paste: remove one
- final int originalLength = mText.length();
- deleteText_internal(min - 1, min);
- // Due to filters, there is no guarantee that exactly one character was
- // removed: count instead.
- final int delta = mText.length() - originalLength;
- min += delta;
- max += delta;
- } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
- !Character.isSpaceChar(charAfter) && charAfter != '\n') {
- // No space at beginning of paste: add one
- final int originalLength = mText.length();
- replaceText_internal(min, min, " ");
- // Taking possible filters into account as above.
- final int delta = mText.length() - originalLength;
- min += delta;
- max += delta;
- }
- }
-
- if (max < mText.length()) {
- final char charBefore = paste.charAt(paste.length() - 1);
- final char charAfter = mTransformed.charAt(max);
-
- if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
- // Two spaces at end of paste: remove one
- deleteText_internal(max, max + 1);
- } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
- !Character.isSpaceChar(charAfter) && charAfter != '\n') {
- // No space at end of paste: add one
- replaceText_internal(max, max, " ");
- }
- }
- }
-
- return TextUtils.packRangeInLong(min, max);
- }
-
- private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
- TextView shadowView = (TextView) inflate(mContext,
- com.android.internal.R.layout.text_drag_thumbnail, null);
-
- if (shadowView == null) {
- throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
- }
-
- if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
- text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
- }
- shadowView.setText(text);
- shadowView.setTextColor(getTextColors());
-
- shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
- shadowView.setGravity(Gravity.CENTER);
-
- shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT));
-
- final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
- shadowView.measure(size, size);
-
- shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
- shadowView.invalidate();
- return new DragShadowBuilder(shadowView);
- }
-
@Override
public boolean performLongClick() {
boolean handled = false;
@@ -8145,179 +7461,27 @@
handled = true;
}
- if (mEditor == null) {
- return handled;
- }
-
- // Long press in empty space moves cursor and shows the Paste affordance if available.
- if (!handled && !isPositionOnText(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY) &&
- getEditor().mInsertionControllerEnabled) {
- final int offset = getOffsetForPosition(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY);
- stopSelectionActionMode();
- Selection.setSelection((Spannable) mText, offset);
- getInsertionController().showWithActionPopup();
- handled = true;
- }
-
- if (!handled && getEditor().mSelectionActionMode != null) {
- if (touchPositionIsInSelection()) {
- // Start a drag
- final int start = getSelectionStart();
- final int end = getSelectionEnd();
- CharSequence selectedText = getTransformedText(start, end);
- ClipData data = ClipData.newPlainText(null, selectedText);
- DragLocalState localState = new DragLocalState(this, start, end);
- startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
- stopSelectionActionMode();
- } else {
- getSelectionController().hide();
- selectCurrentWord();
- getSelectionController().show();
- }
- handled = true;
- }
-
- // Start a new selection
- if (!handled) {
- handled = startSelectionActionMode();
+ if (mEditor != null) {
+ handled |= getEditor().performLongClick(handled);
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- getEditor().mDiscardNextActionUp = true;
+ if (mEditor != null) getEditor().mDiscardNextActionUp = true;
}
return handled;
}
- private boolean touchPositionIsInSelection() {
- int selectionStart = getSelectionStart();
- int selectionEnd = getSelectionEnd();
-
- if (selectionStart == selectionEnd) {
- return false;
- }
-
- if (selectionStart > selectionEnd) {
- int tmp = selectionStart;
- selectionStart = selectionEnd;
- selectionEnd = tmp;
- Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
- }
-
- SelectionModifierCursorController selectionController = getSelectionController();
- int minOffset = selectionController.getMinTouchOffset();
- int maxOffset = selectionController.getMaxTouchOffset();
-
- return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
- }
-
- private PositionListener getPositionListener() {
- if (getEditor().mPositionListener == null) {
- getEditor().mPositionListener = new PositionListener();
- }
- return getEditor().mPositionListener;
- }
-
- private interface TextViewPositionListener {
- public void updatePosition(int parentPositionX, int parentPositionY,
- boolean parentPositionChanged, boolean parentScrolled);
- }
-
- private boolean isPositionVisible(int positionX, int positionY) {
- synchronized (TEMP_POSITION) {
- final float[] position = TEMP_POSITION;
- position[0] = positionX;
- position[1] = positionY;
- View view = this;
-
- while (view != null) {
- if (view != this) {
- // Local scroll is already taken into account in positionX/Y
- position[0] -= view.getScrollX();
- position[1] -= view.getScrollY();
- }
-
- if (position[0] < 0 || position[1] < 0 ||
- position[0] > view.getWidth() || position[1] > view.getHeight()) {
- return false;
- }
-
- if (!view.getMatrix().isIdentity()) {
- view.getMatrix().mapPoints(position);
- }
-
- position[0] += view.getLeft();
- position[1] += view.getTop();
-
- final ViewParent parent = view.getParent();
- if (parent instanceof View) {
- view = (View) parent;
- } else {
- // We've reached the ViewRoot, stop iterating
- view = null;
- }
- }
- }
-
- // We've been able to walk up the view hierarchy and the position was never clipped
- return true;
- }
-
- private boolean isOffsetVisible(int offset) {
- final int line = mLayout.getLineForOffset(offset);
- final int lineBottom = mLayout.getLineBottom(line);
- final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
- return isPositionVisible(primaryHorizontal + viewportToContentHorizontalOffset(),
- lineBottom + viewportToContentVerticalOffset());
- }
-
@Override
protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
if (mEditor != null) {
- if (getEditor().mPositionListener != null) {
- getEditor().mPositionListener.onScrollChanged();
- }
- // Internal scroll affects the clip boundaries
- getEditor().invalidateTextDisplayList();
+ getEditor().onScrollChanged();
}
}
/**
- * Removes the suggestion spans.
- */
- CharSequence removeSuggestionSpans(CharSequence text) {
- if (text instanceof Spanned) {
- Spannable spannable;
- if (text instanceof Spannable) {
- spannable = (Spannable) text;
- } else {
- spannable = new SpannableString(text);
- text = spannable;
- }
-
- SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
- for (int i = 0; i < spans.length; i++) {
- spannable.removeSpan(spans[i]);
- }
- }
- return text;
- }
-
- void showSuggestions() {
- if (getEditor().mSuggestionsPopupWindow == null) {
- getEditor().mSuggestionsPopupWindow = new SuggestionsPopupWindow();
- }
- hideControllers();
- getEditor().mSuggestionsPopupWindow.show();
- }
-
- boolean areSuggestionsShown() {
- return getEditor().mSuggestionsPopupWindow != null && getEditor().mSuggestionsPopupWindow.isShowing();
- }
-
- /**
* Return whether or not suggestions are enabled on this TextView. The suggestions are generated
* by the IME or by the spell checker as the user types. This is done by adding
* {@link SuggestionSpan}s to the text.
@@ -8391,65 +7555,100 @@
}
/**
- *
- * @return true if the selection mode was actually started.
- */
- private boolean startSelectionActionMode() {
- if (getEditor().mSelectionActionMode != null) {
- // Selection action mode is already started
- return false;
- }
-
- if (!canSelectText() || !requestFocus()) {
- Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
- return false;
- }
-
- if (!hasSelection()) {
- // There may already be a selection on device rotation
- if (!selectCurrentWord()) {
- // No word found under cursor or text selection not permitted.
- return false;
- }
- }
-
- boolean willExtract = extractedTextModeWillBeStarted();
-
- // Do not start the action mode when extracted text will show up full screen, which would
- // immediately hide the newly created action bar and would be visually distracting.
- if (!willExtract) {
- ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
- getEditor().mSelectionActionMode = startActionMode(actionModeCallback);
- }
-
- final boolean selectionStarted = getEditor().mSelectionActionMode != null || willExtract;
- if (selectionStarted && !isTextSelectable()) {
- // Show the IME to be able to replace text, except when selecting non editable text.
- final InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) {
- imm.showSoftInput(this, 0, null);
- }
- }
-
- return selectionStarted;
- }
-
- private boolean extractedTextModeWillBeStarted() {
- if (!(this instanceof ExtractEditText)) {
- final InputMethodManager imm = InputMethodManager.peekInstance();
- return imm != null && imm.isFullscreenMode();
- }
- return false;
- }
-
- /**
* @hide
*/
protected void stopSelectionActionMode() {
- if (getEditor().mSelectionActionMode != null) {
- // This will hide the mSelectionModifierCursorController
- getEditor().mSelectionActionMode.finish();
+ getEditor().stopSelectionActionMode();
+ }
+
+ boolean canCut() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
}
+
+ if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean canCopy() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
+ }
+
+ if (mText.length() > 0 && hasSelection()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean canPaste() {
+ return (mText instanceof Editable &&
+ mEditor != null && getEditor().mKeyListener != null &&
+ getSelectionStart() >= 0 &&
+ getSelectionEnd() >= 0 &&
+ ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
+ hasPrimaryClip());
+ }
+
+ boolean selectAllText() {
+ final int length = mText.length();
+ Selection.setSelection((Spannable) mText, 0, length);
+ return length > 0;
+ }
+
+ /**
+ * Prepare text so that there are not zero or two spaces at beginning and end of region defined
+ * by [min, max] when replacing this region by paste.
+ * Note that if there were two spaces (or more) at that position before, they are kept. We just
+ * make sure we do not add an extra one from the paste content.
+ */
+ long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
+ if (paste.length() > 0) {
+ if (min > 0) {
+ final char charBefore = mTransformed.charAt(min - 1);
+ final char charAfter = paste.charAt(0);
+
+ if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
+ // Two spaces at beginning of paste: remove one
+ final int originalLength = mText.length();
+ deleteText_internal(min - 1, min);
+ // Due to filters, there is no guarantee that exactly one character was
+ // removed: count instead.
+ final int delta = mText.length() - originalLength;
+ min += delta;
+ max += delta;
+ } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
+ !Character.isSpaceChar(charAfter) && charAfter != '\n') {
+ // No space at beginning of paste: add one
+ final int originalLength = mText.length();
+ replaceText_internal(min, min, " ");
+ // Taking possible filters into account as above.
+ final int delta = mText.length() - originalLength;
+ min += delta;
+ max += delta;
+ }
+ }
+
+ if (max < mText.length()) {
+ final char charBefore = paste.charAt(paste.length() - 1);
+ final char charAfter = mTransformed.charAt(max);
+
+ if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
+ // Two spaces at end of paste: remove one
+ deleteText_internal(max, max + 1);
+ } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
+ !Character.isSpaceChar(charAfter) && charAfter != '\n') {
+ // No space at end of paste: add one
+ replaceText_internal(max, max, " ");
+ }
+ }
+ }
+
+ return TextUtils.packRangeInLong(min, max);
}
/**
@@ -8489,36 +7688,6 @@
LAST_CUT_OR_COPY_TIME = SystemClock.uptimeMillis();
}
- private void hideInsertionPointCursorController() {
- // No need to create the controller to hide it.
- if (getEditor().mInsertionPointCursorController != null) {
- getEditor().mInsertionPointCursorController.hide();
- }
- }
-
- /**
- * Hides the insertion controller and stops text selection mode, hiding the selection controller
- */
- private void hideControllers() {
- hideCursorControllers();
- hideSpanControllers();
- }
-
- private void hideSpanControllers() {
- if (mChangeWatcher != null) {
- mChangeWatcher.hideControllers();
- }
- }
-
- private void hideCursorControllers() {
- if (getEditor().mSuggestionsPopupWindow != null && !getEditor().mSuggestionsPopupWindow.isShowingUp()) {
- // Should be done before hide insertion point controller since it triggers a show of it
- getEditor().mSuggestionsPopupWindow.hide();
- }
- hideInsertionPointCursorController();
- stopSelectionActionMode();
- }
-
/**
* Get the character offset closest to the specified absolute position. A typical use case is to
* pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
@@ -8535,7 +7704,7 @@
return offset;
}
- private float convertToLocalHorizontalCoordinate(float x) {
+ float convertToLocalHorizontalCoordinate(float x) {
x -= getTotalPaddingLeft();
// Clamp the position to inside of the view.
x = Math.max(0.0f, x);
@@ -8544,7 +7713,7 @@
return x;
}
- private int getLineAtCoordinate(float y) {
+ int getLineAtCoordinate(float y) {
y -= getTotalPaddingTop();
// Clamp the position to inside of the view.
y = Math.max(0.0f, y);
@@ -8558,25 +7727,11 @@
return getLayout().getOffsetForHorizontal(line, x);
}
- /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
- * in the view. Returns false when the position is in the empty space of left/right of text.
- */
- private boolean isPositionOnText(float x, float y) {
- if (getLayout() == null) return false;
-
- final int line = getLineAtCoordinate(y);
- x = convertToLocalHorizontalCoordinate(x);
-
- if (x < getLayout().getLineLeft(line)) return false;
- if (x > getLayout().getLineRight(line)) return false;
- return true;
- }
-
@Override
public boolean onDragEvent(DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
- return mEditor != null && hasInsertionController();
+ return mEditor != null && getEditor().hasInsertionController();
case DragEvent.ACTION_DRAG_ENTERED:
TextView.this.requestFocus();
@@ -8588,7 +7743,7 @@
return true;
case DragEvent.ACTION_DROP:
- onDrop(event);
+ if (mEditor != null) getEditor().onDrop(event);
return true;
case DragEvent.ACTION_DRAG_ENDED:
@@ -8598,112 +7753,9 @@
}
}
- private void onDrop(DragEvent event) {
- StringBuilder content = new StringBuilder("");
- ClipData clipData = event.getClipData();
- final int itemCount = clipData.getItemCount();
- for (int i=0; i < itemCount; i++) {
- Item item = clipData.getItemAt(i);
- content.append(item.coerceToText(TextView.this.mContext));
- }
-
- final int offset = getOffsetForPosition(event.getX(), event.getY());
-
- Object localState = event.getLocalState();
- DragLocalState dragLocalState = null;
- if (localState instanceof DragLocalState) {
- dragLocalState = (DragLocalState) localState;
- }
- boolean dragDropIntoItself = dragLocalState != null &&
- dragLocalState.sourceTextView == this;
-
- if (dragDropIntoItself) {
- if (offset >= dragLocalState.start && offset < dragLocalState.end) {
- // A drop inside the original selection discards the drop.
- return;
- }
- }
-
- final int originalLength = mText.length();
- long minMax = prepareSpacesAroundPaste(offset, offset, content);
- int min = TextUtils.unpackRangeStartFromLong(minMax);
- int max = TextUtils.unpackRangeEndFromLong(minMax);
-
- Selection.setSelection((Spannable) mText, max);
- replaceText_internal(min, max, content);
-
- if (dragDropIntoItself) {
- int dragSourceStart = dragLocalState.start;
- int dragSourceEnd = dragLocalState.end;
- if (max <= dragSourceStart) {
- // Inserting text before selection has shifted positions
- final int shift = mText.length() - originalLength;
- dragSourceStart += shift;
- dragSourceEnd += shift;
- }
-
- // Delete original selection
- deleteText_internal(dragSourceStart, dragSourceEnd);
-
- // Make sure we do not leave two adjacent spaces.
- if ((dragSourceStart == 0 ||
- Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
- (dragSourceStart == mText.length() ||
- Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
- final int pos = dragSourceStart == mText.length() ?
- dragSourceStart - 1 : dragSourceStart;
- deleteText_internal(pos, pos + 1);
- }
- }
- }
-
- /**
- * @return True if this view supports insertion handles.
- */
- boolean hasInsertionController() {
- return getEditor().mInsertionControllerEnabled;
- }
-
- /**
- * @return True if this view supports selection handles.
- */
- boolean hasSelectionController() {
- return getEditor().mSelectionControllerEnabled;
- }
-
- InsertionPointCursorController getInsertionController() {
- if (!getEditor().mInsertionControllerEnabled) {
- return null;
- }
-
- if (getEditor().mInsertionPointCursorController == null) {
- getEditor().mInsertionPointCursorController = new InsertionPointCursorController();
-
- final ViewTreeObserver observer = getViewTreeObserver();
- observer.addOnTouchModeChangeListener(getEditor().mInsertionPointCursorController);
- }
-
- return getEditor().mInsertionPointCursorController;
- }
-
- SelectionModifierCursorController getSelectionController() {
- if (!getEditor().mSelectionControllerEnabled) {
- return null;
- }
-
- if (getEditor().mSelectionModifierCursorController == null) {
- getEditor().mSelectionModifierCursorController = new SelectionModifierCursorController();
-
- final ViewTreeObserver observer = getViewTreeObserver();
- observer.addOnTouchModeChangeListener(getEditor().mSelectionModifierCursorController);
- }
-
- return getEditor().mSelectionModifierCursorController;
- }
-
boolean isInBatchEditMode() {
if (mEditor == null) return false;
- final InputMethodState ims = getEditor().mInputMethodState;
+ final Editor.InputMethodState ims = getEditor().mInputMethodState;
if (ims != null) {
return ims.mBatchEditNesting > 0;
}
@@ -8864,7 +7916,7 @@
if (!(this instanceof EditText)) {
Log.e(LOG_TAG + " EDITOR", "Creating Editor on TextView. " + reason);
}
- mEditor = new Editor();
+ mEditor = new Editor(this);
} else {
if (!(this instanceof EditText)) {
Log.d(LOG_TAG + " EDITOR", "Redundant Editor creation. " + reason);
@@ -9041,151 +8093,6 @@
}
}
- private static class ErrorPopup extends PopupWindow {
- private boolean mAbove = false;
- private final TextView mView;
- private int mPopupInlineErrorBackgroundId = 0;
- private int mPopupInlineErrorAboveBackgroundId = 0;
-
- ErrorPopup(TextView v, int width, int height) {
- super(v, width, height);
- mView = v;
- // Make sure the TextView has a background set as it will be used the first time it is
- // shown and positionned. Initialized with below background, which should have
- // dimensions identical to the above version for this to work (and is more likely).
- mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
- com.android.internal.R.styleable.Theme_errorMessageBackground);
- mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
- }
-
- void fixDirection(boolean above) {
- mAbove = above;
-
- if (above) {
- mPopupInlineErrorAboveBackgroundId =
- getResourceId(mPopupInlineErrorAboveBackgroundId,
- com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
- } else {
- mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
- com.android.internal.R.styleable.Theme_errorMessageBackground);
- }
-
- mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
- mPopupInlineErrorBackgroundId);
- }
-
- private int getResourceId(int currentId, int index) {
- if (currentId == 0) {
- TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
- R.styleable.Theme);
- currentId = styledAttributes.getResourceId(index, 0);
- styledAttributes.recycle();
- }
- return currentId;
- }
-
- @Override
- public void update(int x, int y, int w, int h, boolean force) {
- super.update(x, y, w, h, force);
-
- boolean above = isAboveAnchor();
- if (above != mAbove) {
- fixDirection(above);
- }
- }
- }
-
- private class CorrectionHighlighter {
- private final Path mPath = new Path();
- private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private int mStart, mEnd;
- private long mFadingStartTime;
- private final static int FADE_OUT_DURATION = 400;
-
- public CorrectionHighlighter() {
- mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
- mPaint.setStyle(Paint.Style.FILL);
- }
-
- public void highlight(CorrectionInfo info) {
- mStart = info.getOffset();
- mEnd = mStart + info.getNewText().length();
- mFadingStartTime = SystemClock.uptimeMillis();
-
- if (mStart < 0 || mEnd < 0) {
- stopAnimation();
- }
- }
-
- public void draw(Canvas canvas, int cursorOffsetVertical) {
- if (updatePath() && updatePaint()) {
- if (cursorOffsetVertical != 0) {
- canvas.translate(0, cursorOffsetVertical);
- }
-
- canvas.drawPath(mPath, mPaint);
-
- if (cursorOffsetVertical != 0) {
- canvas.translate(0, -cursorOffsetVertical);
- }
- invalidate(true); // TODO invalidate cursor region only
- } else {
- stopAnimation();
- invalidate(false); // TODO invalidate cursor region only
- }
- }
-
- private boolean updatePaint() {
- final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
- if (duration > FADE_OUT_DURATION) return false;
-
- final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
- final int highlightColorAlpha = Color.alpha(mHighlightColor);
- final int color = (mHighlightColor & 0x00FFFFFF) +
- ((int) (highlightColorAlpha * coef) << 24);
- mPaint.setColor(color);
- return true;
- }
-
- private boolean updatePath() {
- final Layout layout = TextView.this.mLayout;
- if (layout == null) return false;
-
- // Update in case text is edited while the animation is run
- final int length = mText.length();
- int start = Math.min(length, mStart);
- int end = Math.min(length, mEnd);
-
- mPath.reset();
- TextView.this.mLayout.getSelectionPath(start, end, mPath);
- return true;
- }
-
- private void invalidate(boolean delayed) {
- if (TextView.this.mLayout == null) return;
-
- synchronized (TEMP_RECTF) {
- mPath.computeBounds(TEMP_RECTF, false);
-
- int left = getCompoundPaddingLeft();
- int top = getExtendedPaddingTop() + getVerticalOffset(true);
-
- if (delayed) {
- TextView.this.postInvalidateOnAnimation(
- left + (int) TEMP_RECTF.left, top + (int) TEMP_RECTF.top,
- left + (int) TEMP_RECTF.right, top + (int) TEMP_RECTF.bottom);
- } else {
- TextView.this.postInvalidate((int) TEMP_RECTF.left, (int) TEMP_RECTF.top,
- (int) TEMP_RECTF.right, (int) TEMP_RECTF.bottom);
- }
- }
- }
-
- private void stopAnimation() {
- TextView.this.getEditor().mCorrectionHighlighter = null;
- }
- }
-
private static final class Marquee extends Handler {
// TODO: Add an option to configure this
private static final float MARQUEE_DELTA_MAX = 0.07f;
@@ -9322,217 +8229,10 @@
}
}
- /**
- * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
- * pop-up should be displayed.
- */
- private class EasyEditSpanController {
-
- private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
-
- private EasyEditPopupWindow mPopupWindow;
-
- private EasyEditSpan mEasyEditSpan;
-
- private Runnable mHidePopup;
-
- private void hide() {
- if (mPopupWindow != null) {
- mPopupWindow.hide();
- TextView.this.removeCallbacks(mHidePopup);
- }
- removeSpans(mText);
- mEasyEditSpan = null;
- }
-
- /**
- * Monitors the changes in the text.
- *
- * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
- * as the notifications are not sent when a spannable (with spans) is inserted.
- */
- public void onTextChange(CharSequence buffer) {
- adjustSpans(mText);
-
- if (getWindowVisibility() != View.VISIBLE) {
- // The window is not visible yet, ignore the text change.
- return;
- }
-
- if (mLayout == null) {
- // The view has not been layout yet, ignore the text change
- return;
- }
-
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (!(TextView.this instanceof ExtractEditText)
- && imm != null && imm.isFullscreenMode()) {
- // The input is in extract mode. We do not have to handle the easy edit in the
- // original TextView, as the ExtractEditText will do
- return;
- }
-
- // Remove the current easy edit span, as the text changed, and remove the pop-up
- // (if any)
- if (mEasyEditSpan != null) {
- if (mText instanceof Spannable) {
- ((Spannable) mText).removeSpan(mEasyEditSpan);
- }
- mEasyEditSpan = null;
- }
- if (mPopupWindow != null && mPopupWindow.isShowing()) {
- mPopupWindow.hide();
- }
-
- // Display the new easy edit span (if any).
- if (buffer instanceof Spanned) {
- mEasyEditSpan = getSpan((Spanned) buffer);
- if (mEasyEditSpan != null) {
- if (mPopupWindow == null) {
- mPopupWindow = new EasyEditPopupWindow();
- mHidePopup = new Runnable() {
- @Override
- public void run() {
- hide();
- }
- };
- }
- mPopupWindow.show(mEasyEditSpan);
- TextView.this.removeCallbacks(mHidePopup);
- TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
- }
- }
- }
-
- /**
- * Adjusts the spans by removing all of them except the last one.
- */
- private void adjustSpans(CharSequence buffer) {
- // This method enforces that only one easy edit span is attached to the text.
- // A better way to enforce this would be to listen for onSpanAdded, but this method
- // cannot be used in this scenario as no notification is triggered when a text with
- // spans is inserted into a text.
- if (buffer instanceof Spannable) {
- Spannable spannable = (Spannable) buffer;
- EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
- EasyEditSpan.class);
- for (int i = 0; i < spans.length - 1; i++) {
- spannable.removeSpan(spans[i]);
- }
- }
- }
-
- /**
- * Removes all the {@link EasyEditSpan} currently attached.
- */
- private void removeSpans(CharSequence buffer) {
- if (buffer instanceof Spannable) {
- Spannable spannable = (Spannable) buffer;
- EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
- EasyEditSpan.class);
- for (int i = 0; i < spans.length; i++) {
- spannable.removeSpan(spans[i]);
- }
- }
- }
-
- private EasyEditSpan getSpan(Spanned spanned) {
- EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
- EasyEditSpan.class);
- if (easyEditSpans.length == 0) {
- return null;
- } else {
- return easyEditSpans[0];
- }
- }
- }
-
- /**
- * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
- * by {@link EasyEditSpanController}.
- */
- private class EasyEditPopupWindow extends PinnedPopupWindow
- implements OnClickListener {
- private static final int POPUP_TEXT_LAYOUT =
- com.android.internal.R.layout.text_edit_action_popup_text;
- private TextView mDeleteTextView;
- private EasyEditSpan mEasyEditSpan;
-
- @Override
- protected void createPopupWindow() {
- mPopupWindow = new PopupWindow(TextView.this.mContext, null,
- com.android.internal.R.attr.textSelectHandleWindowStyle);
- mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
- mPopupWindow.setClippingEnabled(true);
- }
-
- @Override
- protected void initContentView() {
- LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
- linearLayout.setOrientation(LinearLayout.HORIZONTAL);
- mContentView = linearLayout;
- mContentView.setBackgroundResource(
- com.android.internal.R.drawable.text_edit_side_paste_window);
-
- LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
- getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- LayoutParams wrapContent = new LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-
- mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
- mDeleteTextView.setLayoutParams(wrapContent);
- mDeleteTextView.setText(com.android.internal.R.string.delete);
- mDeleteTextView.setOnClickListener(this);
- mContentView.addView(mDeleteTextView);
- }
-
- public void show(EasyEditSpan easyEditSpan) {
- mEasyEditSpan = easyEditSpan;
- super.show();
- }
-
- @Override
- public void onClick(View view) {
- if (view == mDeleteTextView) {
- Editable editable = (Editable) mText;
- int start = editable.getSpanStart(mEasyEditSpan);
- int end = editable.getSpanEnd(mEasyEditSpan);
- if (start >= 0 && end >= 0) {
- deleteText_internal(start, end);
- }
- }
- }
-
- @Override
- protected int getTextOffset() {
- // Place the pop-up at the end of the span
- Editable editable = (Editable) mText;
- return editable.getSpanEnd(mEasyEditSpan);
- }
-
- @Override
- protected int getVerticalLocalPosition(int line) {
- return mLayout.getLineBottom(line);
- }
-
- @Override
- protected int clipVertically(int positionY) {
- // As we display the pop-up below the span, no vertical clipping is required.
- return positionY;
- }
- }
-
private class ChangeWatcher implements TextWatcher, SpanWatcher {
private CharSequence mBeforeText;
- private EasyEditSpanController mEasyEditSpanController;
-
- private ChangeWatcher() {
- mEasyEditSpanController = new EasyEditSpanController();
- }
-
public void beforeTextChanged(CharSequence buffer, int start,
int before, int after) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
@@ -9547,14 +8247,11 @@
TextView.this.sendBeforeTextChanged(buffer, start, before, after);
}
- public void onTextChanged(CharSequence buffer, int start,
- int before, int after) {
+ public void onTextChanged(CharSequence buffer, int start, int before, int after) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
+ " before=" + before + " after=" + after + ": " + buffer);
TextView.this.handleTextChanged(buffer, start, before, after);
- mEasyEditSpanController.onTextChange(buffer);
-
if (AccessibilityManager.getInstance(mContext).isEnabled() &&
(isFocused() || isSelected() && isShown())) {
sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
@@ -9571,8 +8268,7 @@
}
}
- public void onSpanChanged(Spannable buf,
- Object what, int s, int e, int st, int en) {
+ public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
+ " st=" + st + " en=" + en + " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, st, e, en);
@@ -9589,2264 +8285,5 @@
+ " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, -1, e, -1);
}
-
- private void hideControllers() {
- mEasyEditSpanController.hide();
- }
- }
-
- private static class Blink extends Handler implements Runnable {
- private final WeakReference<TextView> mView;
- private boolean mCancelled;
-
- public Blink(TextView v) {
- mView = new WeakReference<TextView>(v);
- }
-
- public void run() {
- if (mCancelled) {
- return;
- }
-
- removeCallbacks(Blink.this);
-
- TextView tv = mView.get();
-
- if (tv != null && tv.shouldBlink()) {
- if (tv.mLayout != null) {
- tv.invalidateCursorPath();
- }
-
- postAtTime(this, SystemClock.uptimeMillis() + BLINK);
- }
- }
-
- void cancel() {
- if (!mCancelled) {
- removeCallbacks(Blink.this);
- mCancelled = true;
- }
- }
-
- void uncancel() {
- mCancelled = false;
- }
- }
-
- private static class DragLocalState {
- public TextView sourceTextView;
- public int start, end;
-
- public DragLocalState(TextView sourceTextView, int start, int end) {
- this.sourceTextView = sourceTextView;
- this.start = start;
- this.end = end;
- }
- }
-
- private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
- // 3 handles
- // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
- private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
- private TextViewPositionListener[] mPositionListeners =
- new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
- private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
- private boolean mPositionHasChanged = true;
- // Absolute position of the TextView with respect to its parent window
- private int mPositionX, mPositionY;
- private int mNumberOfListeners;
- private boolean mScrollHasChanged;
- final int[] mTempCoords = new int[2];
-
- public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
- if (mNumberOfListeners == 0) {
- updatePosition();
- ViewTreeObserver vto = TextView.this.getViewTreeObserver();
- vto.addOnPreDrawListener(this);
- }
-
- int emptySlotIndex = -1;
- for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
- TextViewPositionListener listener = mPositionListeners[i];
- if (listener == positionListener) {
- return;
- } else if (emptySlotIndex < 0 && listener == null) {
- emptySlotIndex = i;
- }
- }
-
- mPositionListeners[emptySlotIndex] = positionListener;
- mCanMove[emptySlotIndex] = canMove;
- mNumberOfListeners++;
- }
-
- public void removeSubscriber(TextViewPositionListener positionListener) {
- for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
- if (mPositionListeners[i] == positionListener) {
- mPositionListeners[i] = null;
- mNumberOfListeners--;
- break;
- }
- }
-
- if (mNumberOfListeners == 0) {
- ViewTreeObserver vto = TextView.this.getViewTreeObserver();
- vto.removeOnPreDrawListener(this);
- }
- }
-
- public int getPositionX() {
- return mPositionX;
- }
-
- public int getPositionY() {
- return mPositionY;
- }
-
- @Override
- public boolean onPreDraw() {
- updatePosition();
-
- for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
- if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
- TextViewPositionListener positionListener = mPositionListeners[i];
- if (positionListener != null) {
- positionListener.updatePosition(mPositionX, mPositionY,
- mPositionHasChanged, mScrollHasChanged);
- }
- }
- }
-
- mScrollHasChanged = false;
- return true;
- }
-
- private void updatePosition() {
- TextView.this.getLocationInWindow(mTempCoords);
-
- mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
-
- mPositionX = mTempCoords[0];
- mPositionY = mTempCoords[1];
- }
-
- public void onScrollChanged() {
- mScrollHasChanged = true;
- }
- }
-
- private abstract class PinnedPopupWindow implements TextViewPositionListener {
- protected PopupWindow mPopupWindow;
- protected ViewGroup mContentView;
- int mPositionX, mPositionY;
-
- protected abstract void createPopupWindow();
- protected abstract void initContentView();
- protected abstract int getTextOffset();
- protected abstract int getVerticalLocalPosition(int line);
- protected abstract int clipVertically(int positionY);
-
- public PinnedPopupWindow() {
- createPopupWindow();
-
- mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
- mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
- mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
-
- initContentView();
-
- LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- mContentView.setLayoutParams(wrapContent);
-
- mPopupWindow.setContentView(mContentView);
- }
-
- public void show() {
- TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */);
-
- computeLocalPosition();
-
- final PositionListener positionListener = TextView.this.getPositionListener();
- updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
- }
-
- protected void measureContent() {
- final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
- mContentView.measure(
- View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
- View.MeasureSpec.AT_MOST),
- View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
- View.MeasureSpec.AT_MOST));
- }
-
- /* The popup window will be horizontally centered on the getTextOffset() and vertically
- * positioned according to viewportToContentHorizontalOffset.
- *
- * This method assumes that mContentView has properly been measured from its content. */
- private void computeLocalPosition() {
- measureContent();
- final int width = mContentView.getMeasuredWidth();
- final int offset = getTextOffset();
- mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
- mPositionX += viewportToContentHorizontalOffset();
-
- final int line = mLayout.getLineForOffset(offset);
- mPositionY = getVerticalLocalPosition(line);
- mPositionY += viewportToContentVerticalOffset();
- }
-
- private void updatePosition(int parentPositionX, int parentPositionY) {
- int positionX = parentPositionX + mPositionX;
- int positionY = parentPositionY + mPositionY;
-
- positionY = clipVertically(positionY);
-
- // Horizontal clipping
- final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
- final int width = mContentView.getMeasuredWidth();
- positionX = Math.min(displayMetrics.widthPixels - width, positionX);
- positionX = Math.max(0, positionX);
-
- if (isShowing()) {
- mPopupWindow.update(positionX, positionY, -1, -1);
- } else {
- mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
- positionX, positionY);
- }
- }
-
- public void hide() {
- mPopupWindow.dismiss();
- TextView.this.getPositionListener().removeSubscriber(this);
- }
-
- @Override
- public void updatePosition(int parentPositionX, int parentPositionY,
- boolean parentPositionChanged, boolean parentScrolled) {
- // Either parentPositionChanged or parentScrolled is true, check if still visible
- if (isShowing() && isOffsetVisible(getTextOffset())) {
- if (parentScrolled) computeLocalPosition();
- updatePosition(parentPositionX, parentPositionY);
- } else {
- hide();
- }
- }
-
- public boolean isShowing() {
- return mPopupWindow.isShowing();
- }
- }
-
- private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
- private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
- private static final int ADD_TO_DICTIONARY = -1;
- private static final int DELETE_TEXT = -2;
- private SuggestionInfo[] mSuggestionInfos;
- private int mNumberOfSuggestions;
- private boolean mCursorWasVisibleBeforeSuggestions;
- private boolean mIsShowingUp = false;
- private SuggestionAdapter mSuggestionsAdapter;
- private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
- private final HashMap<SuggestionSpan, Integer> mSpansLengths;
-
- private class CustomPopupWindow extends PopupWindow {
- public CustomPopupWindow(Context context, int defStyle) {
- super(context, null, defStyle);
- }
-
- @Override
- public void dismiss() {
- super.dismiss();
-
- TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
-
- // Safe cast since show() checks that mText is an Editable
- ((Spannable) mText).removeSpan(getEditor().mSuggestionRangeSpan);
-
- setCursorVisible(mCursorWasVisibleBeforeSuggestions);
- if (hasInsertionController()) {
- getInsertionController().show();
- }
- }
- }
-
- public SuggestionsPopupWindow() {
- mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible;
- mSuggestionSpanComparator = new SuggestionSpanComparator();
- mSpansLengths = new HashMap<SuggestionSpan, Integer>();
- }
-
- @Override
- protected void createPopupWindow() {
- mPopupWindow = new CustomPopupWindow(TextView.this.mContext,
- com.android.internal.R.attr.textSuggestionsWindowStyle);
- mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
- mPopupWindow.setFocusable(true);
- mPopupWindow.setClippingEnabled(false);
- }
-
- @Override
- protected void initContentView() {
- ListView listView = new ListView(TextView.this.getContext());
- mSuggestionsAdapter = new SuggestionAdapter();
- listView.setAdapter(mSuggestionsAdapter);
- listView.setOnItemClickListener(this);
- mContentView = listView;
-
- // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
- mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
- for (int i = 0; i < mSuggestionInfos.length; i++) {
- mSuggestionInfos[i] = new SuggestionInfo();
- }
- }
-
- public boolean isShowingUp() {
- return mIsShowingUp;
- }
-
- public void onParentLostFocus() {
- mIsShowingUp = false;
- }
-
- private class SuggestionInfo {
- int suggestionStart, suggestionEnd; // range of actual suggestion within text
- SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
- int suggestionIndex; // the index of this suggestion inside suggestionSpan
- SpannableStringBuilder text = new SpannableStringBuilder();
- TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
- android.R.style.TextAppearance_SuggestionHighlight);
- }
-
- private class SuggestionAdapter extends BaseAdapter {
- private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext.
- getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- @Override
- public int getCount() {
- return mNumberOfSuggestions;
- }
-
- @Override
- public Object getItem(int position) {
- return mSuggestionInfos[position];
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- TextView textView = (TextView) convertView;
-
- if (textView == null) {
- textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent,
- false);
- }
-
- final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
- textView.setText(suggestionInfo.text);
-
- if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
- textView.setCompoundDrawablesWithIntrinsicBounds(
- com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
- } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
- textView.setCompoundDrawablesWithIntrinsicBounds(
- com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
- } else {
- textView.setCompoundDrawables(null, null, null, null);
- }
-
- return textView;
- }
- }
-
- private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
- public int compare(SuggestionSpan span1, SuggestionSpan span2) {
- final int flag1 = span1.getFlags();
- final int flag2 = span2.getFlags();
- if (flag1 != flag2) {
- // The order here should match what is used in updateDrawState
- final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
- final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
- final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
- final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
- if (easy1 && !misspelled1) return -1;
- if (easy2 && !misspelled2) return 1;
- if (misspelled1) return -1;
- if (misspelled2) return 1;
- }
-
- return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
- }
- }
-
- /**
- * Returns the suggestion spans that cover the current cursor position. The suggestion
- * spans are sorted according to the length of text that they are attached to.
- */
- private SuggestionSpan[] getSuggestionSpans() {
- int pos = TextView.this.getSelectionStart();
- Spannable spannable = (Spannable) TextView.this.mText;
- SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
-
- mSpansLengths.clear();
- for (SuggestionSpan suggestionSpan : suggestionSpans) {
- int start = spannable.getSpanStart(suggestionSpan);
- int end = spannable.getSpanEnd(suggestionSpan);
- mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
- }
-
- // The suggestions are sorted according to their types (easy correction first, then
- // misspelled) and to the length of the text that they cover (shorter first).
- Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
- return suggestionSpans;
- }
-
- @Override
- public void show() {
- if (!(mText instanceof Editable)) return;
-
- if (updateSuggestions()) {
- mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible;
- setCursorVisible(false);
- mIsShowingUp = true;
- super.show();
- }
- }
-
- @Override
- protected void measureContent() {
- final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
- final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
- displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
- final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
- displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
-
- int width = 0;
- View view = null;
- for (int i = 0; i < mNumberOfSuggestions; i++) {
- view = mSuggestionsAdapter.getView(i, view, mContentView);
- view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
- view.measure(horizontalMeasure, verticalMeasure);
- width = Math.max(width, view.getMeasuredWidth());
- }
-
- // Enforce the width based on actual text widths
- mContentView.measure(
- View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
- verticalMeasure);
-
- Drawable popupBackground = mPopupWindow.getBackground();
- if (popupBackground != null) {
- if (mTempRect == null) mTempRect = new Rect();
- popupBackground.getPadding(mTempRect);
- width += mTempRect.left + mTempRect.right;
- }
- mPopupWindow.setWidth(width);
- }
-
- @Override
- protected int getTextOffset() {
- return getSelectionStart();
- }
-
- @Override
- protected int getVerticalLocalPosition(int line) {
- return mLayout.getLineBottom(line);
- }
-
- @Override
- protected int clipVertically(int positionY) {
- final int height = mContentView.getMeasuredHeight();
- final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
- return Math.min(positionY, displayMetrics.heightPixels - height);
- }
-
- @Override
- public void hide() {
- super.hide();
- }
-
- private boolean updateSuggestions() {
- Spannable spannable = (Spannable) TextView.this.mText;
- SuggestionSpan[] suggestionSpans = getSuggestionSpans();
-
- final int nbSpans = suggestionSpans.length;
- // Suggestions are shown after a delay: the underlying spans may have been removed
- if (nbSpans == 0) return false;
-
- mNumberOfSuggestions = 0;
- int spanUnionStart = mText.length();
- int spanUnionEnd = 0;
-
- SuggestionSpan misspelledSpan = null;
- int underlineColor = 0;
-
- for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
- SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
- final int spanStart = spannable.getSpanStart(suggestionSpan);
- final int spanEnd = spannable.getSpanEnd(suggestionSpan);
- spanUnionStart = Math.min(spanStart, spanUnionStart);
- spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
-
- if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
- misspelledSpan = suggestionSpan;
- }
-
- // The first span dictates the background color of the highlighted text
- if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
-
- String[] suggestions = suggestionSpan.getSuggestions();
- int nbSuggestions = suggestions.length;
- for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
- String suggestion = suggestions[suggestionIndex];
-
- boolean suggestionIsDuplicate = false;
- for (int i = 0; i < mNumberOfSuggestions; i++) {
- if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
- SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
- final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
- final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
- if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
- suggestionIsDuplicate = true;
- break;
- }
- }
- }
-
- if (!suggestionIsDuplicate) {
- SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
- suggestionInfo.suggestionSpan = suggestionSpan;
- suggestionInfo.suggestionIndex = suggestionIndex;
- suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
-
- mNumberOfSuggestions++;
-
- if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
- // Also end outer for loop
- spanIndex = nbSpans;
- break;
- }
- }
- }
- }
-
- for (int i = 0; i < mNumberOfSuggestions; i++) {
- highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
- }
-
- // Add "Add to dictionary" item if there is a span with the misspelled flag
- if (misspelledSpan != null) {
- final int misspelledStart = spannable.getSpanStart(misspelledSpan);
- final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
- if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
- SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
- suggestionInfo.suggestionSpan = misspelledSpan;
- suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
- suggestionInfo.text.replace(0, suggestionInfo.text.length(),
- getContext().getString(com.android.internal.R.string.addToDictionary));
- suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- mNumberOfSuggestions++;
- }
- }
-
- // Delete item
- SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
- suggestionInfo.suggestionSpan = null;
- suggestionInfo.suggestionIndex = DELETE_TEXT;
- suggestionInfo.text.replace(0, suggestionInfo.text.length(),
- getContext().getString(com.android.internal.R.string.deleteText));
- suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- mNumberOfSuggestions++;
-
- if (getEditor().mSuggestionRangeSpan == null) getEditor().mSuggestionRangeSpan = new SuggestionRangeSpan();
- if (underlineColor == 0) {
- // Fallback on the default highlight color when the first span does not provide one
- getEditor().mSuggestionRangeSpan.setBackgroundColor(mHighlightColor);
- } else {
- final float BACKGROUND_TRANSPARENCY = 0.4f;
- final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
- getEditor().mSuggestionRangeSpan.setBackgroundColor(
- (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
- }
- spannable.setSpan(getEditor().mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- mSuggestionsAdapter.notifyDataSetChanged();
- return true;
- }
-
- private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
- int unionEnd) {
- final Spannable text = (Spannable) mText;
- final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
- final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
-
- // Adjust the start/end of the suggestion span
- suggestionInfo.suggestionStart = spanStart - unionStart;
- suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
- + suggestionInfo.text.length();
-
- suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
- suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- // Add the text before and after the span.
- final String textAsString = text.toString();
- suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
- suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- Editable editable = (Editable) mText;
- SuggestionInfo suggestionInfo = mSuggestionInfos[position];
-
- if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
- final int spanUnionStart = editable.getSpanStart(getEditor().mSuggestionRangeSpan);
- int spanUnionEnd = editable.getSpanEnd(getEditor().mSuggestionRangeSpan);
- if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
- // Do not leave two adjacent spaces after deletion, or one at beginning of text
- if (spanUnionEnd < editable.length() &&
- Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
- (spanUnionStart == 0 ||
- Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
- spanUnionEnd = spanUnionEnd + 1;
- }
- deleteText_internal(spanUnionStart, spanUnionEnd);
- }
- hide();
- return;
- }
-
- final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
- final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
- if (spanStart < 0 || spanEnd <= spanStart) {
- // Span has been removed
- hide();
- return;
- }
- final String originalText = mText.toString().substring(spanStart, spanEnd);
-
- if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
- Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
- intent.putExtra("word", originalText);
- intent.putExtra("locale", getTextServicesLocale().toString());
- intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
- getContext().startActivity(intent);
- // There is no way to know if the word was indeed added. Re-check.
- // TODO The ExtractEditText should remove the span in the original text instead
- editable.removeSpan(suggestionInfo.suggestionSpan);
- updateSpellCheckSpans(spanStart, spanEnd, false);
- } else {
- // SuggestionSpans are removed by replace: save them before
- SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
- SuggestionSpan.class);
- final int length = suggestionSpans.length;
- int[] suggestionSpansStarts = new int[length];
- int[] suggestionSpansEnds = new int[length];
- int[] suggestionSpansFlags = new int[length];
- for (int i = 0; i < length; i++) {
- final SuggestionSpan suggestionSpan = suggestionSpans[i];
- suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
- suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
- suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
-
- // Remove potential misspelled flags
- int suggestionSpanFlags = suggestionSpan.getFlags();
- if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
- suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
- suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
- suggestionSpan.setFlags(suggestionSpanFlags);
- }
- }
-
- final int suggestionStart = suggestionInfo.suggestionStart;
- final int suggestionEnd = suggestionInfo.suggestionEnd;
- final String suggestion = suggestionInfo.text.subSequence(
- suggestionStart, suggestionEnd).toString();
- replaceText_internal(spanStart, spanEnd, suggestion);
-
- // Notify source IME of the suggestion pick. Do this before swaping texts.
- if (!TextUtils.isEmpty(
- suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) {
- imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
- suggestionInfo.suggestionIndex);
- }
- }
-
- // Swap text content between actual text and Suggestion span
- String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
- suggestions[suggestionInfo.suggestionIndex] = originalText;
-
- // Restore previous SuggestionSpans
- final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
- for (int i = 0; i < length; i++) {
- // Only spans that include the modified region make sense after replacement
- // Spans partially included in the replaced region are removed, there is no
- // way to assign them a valid range after replacement
- if (suggestionSpansStarts[i] <= spanStart &&
- suggestionSpansEnds[i] >= spanEnd) {
- setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
- suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
- }
- }
-
- // Move cursor at the end of the replaced word
- final int newCursorPosition = spanEnd + lengthDifference;
- setCursorPosition_internal(newCursorPosition, newCursorPosition);
- }
-
- hide();
- }
- }
-
- /**
- * An ActionMode Callback class that is used to provide actions while in text selection mode.
- *
- * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
- * on which of these this TextView supports.
- */
- private class SelectionActionModeCallback implements ActionMode.Callback {
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- TypedArray styledAttributes = mContext.obtainStyledAttributes(
- com.android.internal.R.styleable.SelectionModeDrawables);
-
- boolean allowText = getContext().getResources().getBoolean(
- com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
-
- mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle));
- mode.setSubtitle(null);
- mode.setTitleOptionalHint(true);
-
- int selectAllIconId = 0; // No icon by default
- if (!allowText) {
- // Provide an icon, text will not be displayed on smaller screens.
- selectAllIconId = styledAttributes.getResourceId(
- R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
- }
-
- menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
- setIcon(selectAllIconId).
- setAlphabeticShortcut('a').
- setShowAsAction(
- MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
-
- if (canCut()) {
- menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
- setIcon(styledAttributes.getResourceId(
- R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
- setAlphabeticShortcut('x').
- setShowAsAction(
- MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
- }
-
- if (canCopy()) {
- menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
- setIcon(styledAttributes.getResourceId(
- R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
- setAlphabeticShortcut('c').
- setShowAsAction(
- MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
- }
-
- if (canPaste()) {
- menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
- setIcon(styledAttributes.getResourceId(
- R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
- setAlphabeticShortcut('v').
- setShowAsAction(
- MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
- }
-
- styledAttributes.recycle();
-
- if (getEditor().mCustomSelectionActionModeCallback != null) {
- if (!getEditor().mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
- // The custom mode can choose to cancel the action mode
- return false;
- }
- }
-
- if (menu.hasVisibleItems() || mode.getCustomView() != null) {
- getSelectionController().show();
- return true;
- } else {
- return false;
- }
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- if (getEditor().mCustomSelectionActionModeCallback != null) {
- return getEditor().mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
- }
- return true;
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- if (getEditor().mCustomSelectionActionModeCallback != null &&
- getEditor().mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
- return true;
- }
- return onTextContextMenuItem(item.getItemId());
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- if (getEditor().mCustomSelectionActionModeCallback != null) {
- getEditor().mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
- }
- Selection.setSelection((Spannable) mText, getSelectionEnd());
-
- if (getEditor().mSelectionModifierCursorController != null) {
- getEditor().mSelectionModifierCursorController.hide();
- }
-
- getEditor().mSelectionActionMode = null;
- }
- }
-
- private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
- private static final int POPUP_TEXT_LAYOUT =
- com.android.internal.R.layout.text_edit_action_popup_text;
- private TextView mPasteTextView;
- private TextView mReplaceTextView;
-
- @Override
- protected void createPopupWindow() {
- mPopupWindow = new PopupWindow(TextView.this.mContext, null,
- com.android.internal.R.attr.textSelectHandleWindowStyle);
- mPopupWindow.setClippingEnabled(true);
- }
-
- @Override
- protected void initContentView() {
- LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
- linearLayout.setOrientation(LinearLayout.HORIZONTAL);
- mContentView = linearLayout;
- mContentView.setBackgroundResource(
- com.android.internal.R.drawable.text_edit_paste_window);
-
- LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
- getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- LayoutParams wrapContent = new LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-
- mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
- mPasteTextView.setLayoutParams(wrapContent);
- mContentView.addView(mPasteTextView);
- mPasteTextView.setText(com.android.internal.R.string.paste);
- mPasteTextView.setOnClickListener(this);
-
- mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
- mReplaceTextView.setLayoutParams(wrapContent);
- mContentView.addView(mReplaceTextView);
- mReplaceTextView.setText(com.android.internal.R.string.replace);
- mReplaceTextView.setOnClickListener(this);
- }
-
- @Override
- public void show() {
- boolean canPaste = canPaste();
- boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
- mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
- mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
-
- if (!canPaste && !canSuggest) return;
-
- super.show();
- }
-
- @Override
- public void onClick(View view) {
- if (view == mPasteTextView && canPaste()) {
- onTextContextMenuItem(ID_PASTE);
- hide();
- } else if (view == mReplaceTextView) {
- final int middle = (getSelectionStart() + getSelectionEnd()) / 2;
- stopSelectionActionMode();
- Selection.setSelection((Spannable) mText, middle);
- showSuggestions();
- }
- }
-
- @Override
- protected int getTextOffset() {
- return (getSelectionStart() + getSelectionEnd()) / 2;
- }
-
- @Override
- protected int getVerticalLocalPosition(int line) {
- return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
- }
-
- @Override
- protected int clipVertically(int positionY) {
- if (positionY < 0) {
- final int offset = getTextOffset();
- final int line = mLayout.getLineForOffset(offset);
- positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
- positionY += mContentView.getMeasuredHeight();
-
- // Assumes insertion and selection handles share the same height
- final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
- positionY += handle.getIntrinsicHeight();
- }
-
- return positionY;
- }
- }
-
- private abstract class HandleView extends View implements TextViewPositionListener {
- protected Drawable mDrawable;
- protected Drawable mDrawableLtr;
- protected Drawable mDrawableRtl;
- private final PopupWindow mContainer;
- // Position with respect to the parent TextView
- private int mPositionX, mPositionY;
- private boolean mIsDragging;
- // Offset from touch position to mPosition
- private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
- protected int mHotspotX;
- // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
- private float mTouchOffsetY;
- // Where the touch position should be on the handle to ensure a maximum cursor visibility
- private float mIdealVerticalOffset;
- // Parent's (TextView) previous position in window
- private int mLastParentX, mLastParentY;
- // Transient action popup window for Paste and Replace actions
- protected ActionPopupWindow mActionPopupWindow;
- // Previous text character offset
- private int mPreviousOffset = -1;
- // Previous text character offset
- private boolean mPositionHasChanged = true;
- // Used to delay the appearance of the action popup window
- private Runnable mActionPopupShower;
-
- public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
- super(TextView.this.mContext);
- mContainer = new PopupWindow(TextView.this.mContext, null,
- com.android.internal.R.attr.textSelectHandleWindowStyle);
- mContainer.setSplitTouchEnabled(true);
- mContainer.setClippingEnabled(false);
- mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
- mContainer.setContentView(this);
-
- mDrawableLtr = drawableLtr;
- mDrawableRtl = drawableRtl;
-
- updateDrawable();
-
- final int handleHeight = mDrawable.getIntrinsicHeight();
- mTouchOffsetY = -0.3f * handleHeight;
- mIdealVerticalOffset = 0.7f * handleHeight;
- }
-
- protected void updateDrawable() {
- final int offset = getCurrentCursorOffset();
- final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset);
- mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
- mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
- }
-
- protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
-
- // Touch-up filter: number of previous positions remembered
- private static final int HISTORY_SIZE = 5;
- private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
- private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
- private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
- private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
- private int mPreviousOffsetIndex = 0;
- private int mNumberPreviousOffsets = 0;
-
- private void startTouchUpFilter(int offset) {
- mNumberPreviousOffsets = 0;
- addPositionToTouchUpFilter(offset);
- }
-
- private void addPositionToTouchUpFilter(int offset) {
- mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
- mPreviousOffsets[mPreviousOffsetIndex] = offset;
- mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
- mNumberPreviousOffsets++;
- }
-
- private void filterOnTouchUp() {
- final long now = SystemClock.uptimeMillis();
- int i = 0;
- int index = mPreviousOffsetIndex;
- final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
- while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
- i++;
- index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
- }
-
- if (i > 0 && i < iMax &&
- (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
- positionAtCursorOffset(mPreviousOffsets[index], false);
- }
- }
-
- public boolean offsetHasBeenChanged() {
- return mNumberPreviousOffsets > 1;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
- }
-
- public void show() {
- if (isShowing()) return;
-
- getPositionListener().addSubscriber(this, true /* local position may change */);
-
- // Make sure the offset is always considered new, even when focusing at same position
- mPreviousOffset = -1;
- positionAtCursorOffset(getCurrentCursorOffset(), false);
-
- hideActionPopupWindow();
- }
-
- protected void dismiss() {
- mIsDragging = false;
- mContainer.dismiss();
- onDetached();
- }
-
- public void hide() {
- dismiss();
-
- TextView.this.getPositionListener().removeSubscriber(this);
- }
-
- void showActionPopupWindow(int delay) {
- if (mActionPopupWindow == null) {
- mActionPopupWindow = new ActionPopupWindow();
- }
- if (mActionPopupShower == null) {
- mActionPopupShower = new Runnable() {
- public void run() {
- mActionPopupWindow.show();
- }
- };
- } else {
- TextView.this.removeCallbacks(mActionPopupShower);
- }
- TextView.this.postDelayed(mActionPopupShower, delay);
- }
-
- protected void hideActionPopupWindow() {
- if (mActionPopupShower != null) {
- TextView.this.removeCallbacks(mActionPopupShower);
- }
- if (mActionPopupWindow != null) {
- mActionPopupWindow.hide();
- }
- }
-
- public boolean isShowing() {
- return mContainer.isShowing();
- }
-
- private boolean isVisible() {
- // Always show a dragging handle.
- if (mIsDragging) {
- return true;
- }
-
- if (isInBatchEditMode()) {
- return false;
- }
-
- return TextView.this.isPositionVisible(mPositionX + mHotspotX, mPositionY);
- }
-
- public abstract int getCurrentCursorOffset();
-
- protected abstract void updateSelection(int offset);
-
- public abstract void updatePosition(float x, float y);
-
- protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
- // A HandleView relies on the layout, which may be nulled by external methods
- if (mLayout == null) {
- // Will update controllers' state, hiding them and stopping selection mode if needed
- prepareCursorControllers();
- return;
- }
-
- boolean offsetChanged = offset != mPreviousOffset;
- if (offsetChanged || parentScrolled) {
- if (offsetChanged) {
- updateSelection(offset);
- addPositionToTouchUpFilter(offset);
- }
- final int line = mLayout.getLineForOffset(offset);
-
- mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
- mPositionY = mLayout.getLineBottom(line);
-
- // Take TextView's padding and scroll into account.
- mPositionX += viewportToContentHorizontalOffset();
- mPositionY += viewportToContentVerticalOffset();
-
- mPreviousOffset = offset;
- mPositionHasChanged = true;
- }
- }
-
- public void updatePosition(int parentPositionX, int parentPositionY,
- boolean parentPositionChanged, boolean parentScrolled) {
- positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
- if (parentPositionChanged || mPositionHasChanged) {
- if (mIsDragging) {
- // Update touchToWindow offset in case of parent scrolling while dragging
- if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
- mTouchToWindowOffsetX += parentPositionX - mLastParentX;
- mTouchToWindowOffsetY += parentPositionY - mLastParentY;
- mLastParentX = parentPositionX;
- mLastParentY = parentPositionY;
- }
-
- onHandleMoved();
- }
-
- if (isVisible()) {
- final int positionX = parentPositionX + mPositionX;
- final int positionY = parentPositionY + mPositionY;
- if (isShowing()) {
- mContainer.update(positionX, positionY, -1, -1);
- } else {
- mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
- positionX, positionY);
- }
- } else {
- if (isShowing()) {
- dismiss();
- }
- }
-
- mPositionHasChanged = false;
- }
- }
-
- @Override
- protected void onDraw(Canvas c) {
- mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
- mDrawable.draw(c);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- startTouchUpFilter(getCurrentCursorOffset());
- mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
- mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
-
- final PositionListener positionListener = getPositionListener();
- mLastParentX = positionListener.getPositionX();
- mLastParentY = positionListener.getPositionY();
- mIsDragging = true;
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- final float rawX = ev.getRawX();
- final float rawY = ev.getRawY();
-
- // Vertical hysteresis: vertical down movement tends to snap to ideal offset
- final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
- final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
- float newVerticalOffset;
- if (previousVerticalOffset < mIdealVerticalOffset) {
- newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
- newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
- } else {
- newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
- newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
- }
- mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
-
- final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
- final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
-
- updatePosition(newPosX, newPosY);
- break;
- }
-
- case MotionEvent.ACTION_UP:
- filterOnTouchUp();
- mIsDragging = false;
- break;
-
- case MotionEvent.ACTION_CANCEL:
- mIsDragging = false;
- break;
- }
- return true;
- }
-
- public boolean isDragging() {
- return mIsDragging;
- }
-
- void onHandleMoved() {
- hideActionPopupWindow();
- }
-
- public void onDetached() {
- hideActionPopupWindow();
- }
- }
-
- private class InsertionHandleView extends HandleView {
- private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
- private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
-
- // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
- private float mDownPositionX, mDownPositionY;
- private Runnable mHider;
-
- public InsertionHandleView(Drawable drawable) {
- super(drawable, drawable);
- }
-
- @Override
- public void show() {
- super.show();
-
- final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - LAST_CUT_OR_COPY_TIME;
- if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
- showActionPopupWindow(0);
- }
-
- hideAfterDelay();
- }
-
- public void showWithActionPopup() {
- show();
- showActionPopupWindow(0);
- }
-
- private void hideAfterDelay() {
- if (mHider == null) {
- mHider = new Runnable() {
- public void run() {
- hide();
- }
- };
- } else {
- removeHiderCallback();
- }
- TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
- }
-
- private void removeHiderCallback() {
- if (mHider != null) {
- TextView.this.removeCallbacks(mHider);
- }
- }
-
- @Override
- protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
- return drawable.getIntrinsicWidth() / 2;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- final boolean result = super.onTouchEvent(ev);
-
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- mDownPositionX = ev.getRawX();
- mDownPositionY = ev.getRawY();
- break;
-
- case MotionEvent.ACTION_UP:
- if (!offsetHasBeenChanged()) {
- final float deltaX = mDownPositionX - ev.getRawX();
- final float deltaY = mDownPositionY - ev.getRawY();
- final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
- final ViewConfiguration viewConfiguration = ViewConfiguration.get(
- TextView.this.getContext());
- final int touchSlop = viewConfiguration.getScaledTouchSlop();
-
- if (distanceSquared < touchSlop * touchSlop) {
- if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
- // Tapping on the handle dismisses the displayed action popup
- mActionPopupWindow.hide();
- } else {
- showWithActionPopup();
- }
- }
- }
- hideAfterDelay();
- break;
-
- case MotionEvent.ACTION_CANCEL:
- hideAfterDelay();
- break;
-
- default:
- break;
- }
-
- return result;
- }
-
- @Override
- public int getCurrentCursorOffset() {
- return TextView.this.getSelectionStart();
- }
-
- @Override
- public void updateSelection(int offset) {
- Selection.setSelection((Spannable) mText, offset);
- }
-
- @Override
- public void updatePosition(float x, float y) {
- positionAtCursorOffset(getOffsetForPosition(x, y), false);
- }
-
- @Override
- void onHandleMoved() {
- super.onHandleMoved();
- removeHiderCallback();
- }
-
- @Override
- public void onDetached() {
- super.onDetached();
- removeHiderCallback();
- }
- }
-
- private class SelectionStartHandleView extends HandleView {
-
- public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
- super(drawableLtr, drawableRtl);
- }
-
- @Override
- protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
- if (isRtlRun) {
- return drawable.getIntrinsicWidth() / 4;
- } else {
- return (drawable.getIntrinsicWidth() * 3) / 4;
- }
- }
-
- @Override
- public int getCurrentCursorOffset() {
- return TextView.this.getSelectionStart();
- }
-
- @Override
- public void updateSelection(int offset) {
- Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
- updateDrawable();
- }
-
- @Override
- public void updatePosition(float x, float y) {
- int offset = getOffsetForPosition(x, y);
-
- // Handles can not cross and selection is at least one character
- final int selectionEnd = getSelectionEnd();
- if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
-
- positionAtCursorOffset(offset, false);
- }
-
- public ActionPopupWindow getActionPopupWindow() {
- return mActionPopupWindow;
- }
- }
-
- private class SelectionEndHandleView extends HandleView {
-
- public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
- super(drawableLtr, drawableRtl);
- }
-
- @Override
- protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
- if (isRtlRun) {
- return (drawable.getIntrinsicWidth() * 3) / 4;
- } else {
- return drawable.getIntrinsicWidth() / 4;
- }
- }
-
- @Override
- public int getCurrentCursorOffset() {
- return TextView.this.getSelectionEnd();
- }
-
- @Override
- public void updateSelection(int offset) {
- Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
- updateDrawable();
- }
-
- @Override
- public void updatePosition(float x, float y) {
- int offset = getOffsetForPosition(x, y);
-
- // Handles can not cross and selection is at least one character
- final int selectionStart = getSelectionStart();
- if (offset <= selectionStart) offset = Math.min(selectionStart + 1, mText.length());
-
- positionAtCursorOffset(offset, false);
- }
-
- public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
- mActionPopupWindow = actionPopupWindow;
- }
- }
-
- /**
- * A CursorController instance can be used to control a cursor in the text.
- * It is not used outside of {@link TextView}.
- * @hide
- */
- private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
- /**
- * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
- * See also {@link #hide()}.
- */
- public void show();
-
- /**
- * Hide the cursor controller from screen.
- * See also {@link #show()}.
- */
- public void hide();
-
- /**
- * Called when the view is detached from window. Perform house keeping task, such as
- * stopping Runnable thread that would otherwise keep a reference on the context, thus
- * preventing the activity from being recycled.
- */
- public void onDetached();
- }
-
- private class InsertionPointCursorController implements CursorController {
- private InsertionHandleView mHandle;
-
- public void show() {
- getHandle().show();
- }
-
- public void showWithActionPopup() {
- getHandle().showWithActionPopup();
- }
-
- public void hide() {
- if (mHandle != null) {
- mHandle.hide();
- }
- }
-
- public void onTouchModeChanged(boolean isInTouchMode) {
- if (!isInTouchMode) {
- hide();
- }
- }
-
- private InsertionHandleView getHandle() {
- if (getEditor().mSelectHandleCenter == null) {
- getEditor().mSelectHandleCenter = mContext.getResources().getDrawable(
- mTextSelectHandleRes);
- }
- if (mHandle == null) {
- mHandle = new InsertionHandleView(getEditor().mSelectHandleCenter);
- }
- return mHandle;
- }
-
- @Override
- public void onDetached() {
- final ViewTreeObserver observer = getViewTreeObserver();
- observer.removeOnTouchModeChangeListener(this);
-
- if (mHandle != null) mHandle.onDetached();
- }
- }
-
- private class SelectionModifierCursorController implements CursorController {
- private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
- // The cursor controller handles, lazily created when shown.
- private SelectionStartHandleView mStartHandle;
- private SelectionEndHandleView mEndHandle;
- // The offsets of that last touch down event. Remembered to start selection there.
- private int mMinTouchOffset, mMaxTouchOffset;
-
- // Double tap detection
- private long mPreviousTapUpTime = 0;
- private float mDownPositionX, mDownPositionY;
- private boolean mGestureStayedInTapRegion;
-
- SelectionModifierCursorController() {
- resetTouchOffsets();
- }
-
- public void show() {
- if (isInBatchEditMode()) {
- return;
- }
- initDrawables();
- initHandles();
- hideInsertionPointCursorController();
- }
-
- private void initDrawables() {
- if (getEditor().mSelectHandleLeft == null) {
- getEditor().mSelectHandleLeft = mContext.getResources().getDrawable(
- mTextSelectHandleLeftRes);
- }
- if (getEditor().mSelectHandleRight == null) {
- getEditor().mSelectHandleRight = mContext.getResources().getDrawable(
- mTextSelectHandleRightRes);
- }
- }
-
- private void initHandles() {
- // Lazy object creation has to be done before updatePosition() is called.
- if (mStartHandle == null) {
- mStartHandle = new SelectionStartHandleView(getEditor().mSelectHandleLeft, getEditor().mSelectHandleRight);
- }
- if (mEndHandle == null) {
- mEndHandle = new SelectionEndHandleView(getEditor().mSelectHandleRight, getEditor().mSelectHandleLeft);
- }
-
- mStartHandle.show();
- mEndHandle.show();
-
- // Make sure both left and right handles share the same ActionPopupWindow (so that
- // moving any of the handles hides the action popup).
- mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
- mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
-
- hideInsertionPointCursorController();
- }
-
- public void hide() {
- if (mStartHandle != null) mStartHandle.hide();
- if (mEndHandle != null) mEndHandle.hide();
- }
-
- public void onTouchEvent(MotionEvent event) {
- // This is done even when the View does not have focus, so that long presses can start
- // selection and tap can move cursor from this tap position.
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- final float x = event.getX();
- final float y = event.getY();
-
- // Remember finger down position, to be able to start selection from there
- mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
-
- // Double tap detection
- if (mGestureStayedInTapRegion) {
- long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
- if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
- final float deltaX = x - mDownPositionX;
- final float deltaY = y - mDownPositionY;
- final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
- ViewConfiguration viewConfiguration = ViewConfiguration.get(
- TextView.this.getContext());
- int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
- boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
-
- if (stayedInArea && isPositionOnText(x, y)) {
- startSelectionActionMode();
- getEditor().mDiscardNextActionUp = true;
- }
- }
- }
-
- mDownPositionX = x;
- mDownPositionY = y;
- mGestureStayedInTapRegion = true;
- break;
-
- case MotionEvent.ACTION_POINTER_DOWN:
- case MotionEvent.ACTION_POINTER_UP:
- // Handle multi-point gestures. Keep min and max offset positions.
- // Only activated for devices that correctly handle multi-touch.
- if (mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
- updateMinAndMaxOffsets(event);
- }
- break;
-
- case MotionEvent.ACTION_MOVE:
- if (mGestureStayedInTapRegion) {
- final float deltaX = event.getX() - mDownPositionX;
- final float deltaY = event.getY() - mDownPositionY;
- final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
- final ViewConfiguration viewConfiguration = ViewConfiguration.get(
- TextView.this.getContext());
- int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
-
- if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
- mGestureStayedInTapRegion = false;
- }
- }
- break;
-
- case MotionEvent.ACTION_UP:
- mPreviousTapUpTime = SystemClock.uptimeMillis();
- break;
- }
- }
-
- /**
- * @param event
- */
- private void updateMinAndMaxOffsets(MotionEvent event) {
- int pointerCount = event.getPointerCount();
- for (int index = 0; index < pointerCount; index++) {
- int offset = getOffsetForPosition(event.getX(index), event.getY(index));
- if (offset < mMinTouchOffset) mMinTouchOffset = offset;
- if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
- }
- }
-
- public int getMinTouchOffset() {
- return mMinTouchOffset;
- }
-
- public int getMaxTouchOffset() {
- return mMaxTouchOffset;
- }
-
- public void resetTouchOffsets() {
- mMinTouchOffset = mMaxTouchOffset = -1;
- }
-
- /**
- * @return true iff this controller is currently used to move the selection start.
- */
- public boolean isSelectionStartDragged() {
- return mStartHandle != null && mStartHandle.isDragging();
- }
-
- public void onTouchModeChanged(boolean isInTouchMode) {
- if (!isInTouchMode) {
- hide();
- }
- }
-
- @Override
- public void onDetached() {
- final ViewTreeObserver observer = getViewTreeObserver();
- observer.removeOnTouchModeChangeListener(this);
-
- if (mStartHandle != null) mStartHandle.onDetached();
- if (mEndHandle != null) mEndHandle.onDetached();
- }
- }
-
- static class InputContentType {
- int imeOptions = EditorInfo.IME_NULL;
- String privateImeOptions;
- CharSequence imeActionLabel;
- int imeActionId;
- Bundle extras;
- OnEditorActionListener onEditorActionListener;
- boolean enterDown;
- }
-
- static class InputMethodState {
- Rect mCursorRectInWindow = new Rect();
- RectF mTmpRectF = new RectF();
- float[] mTmpOffset = new float[2];
- ExtractedTextRequest mExtracting;
- final ExtractedText mTmpExtracted = new ExtractedText();
- int mBatchEditNesting;
- boolean mCursorChanged;
- boolean mSelectionModeChanged;
- boolean mContentChanged;
- int mChangedStart, mChangedEnd, mChangedDelta;
- }
-
- private class Editor {
- // Cursor Controllers.
- InsertionPointCursorController mInsertionPointCursorController;
- SelectionModifierCursorController mSelectionModifierCursorController;
- ActionMode mSelectionActionMode;
- boolean mInsertionControllerEnabled;
- boolean mSelectionControllerEnabled;
-
- // Used to highlight a word when it is corrected by the IME
- CorrectionHighlighter mCorrectionHighlighter;
-
- InputContentType mInputContentType;
- InputMethodState mInputMethodState;
-
- DisplayList[] mTextDisplayLists;
-
- boolean mFrozenWithFocus;
- boolean mSelectionMoved;
- boolean mTouchFocusSelected;
-
- KeyListener mKeyListener;
- int mInputType = EditorInfo.TYPE_NULL;
-
- boolean mDiscardNextActionUp;
- boolean mIgnoreActionUpEvent;
-
- long mShowCursor;
- Blink mBlink;
-
- boolean mCursorVisible = true;
- boolean mSelectAllOnFocus;
- boolean mTextIsSelectable;
-
- CharSequence mError;
- boolean mErrorWasChanged;
- ErrorPopup mErrorPopup;
- /**
- * This flag is set if the TextView tries to display an error before it
- * is attached to the window (so its position is still unknown).
- * It causes the error to be shown later, when onAttachedToWindow()
- * is called.
- */
- boolean mShowErrorAfterAttach;
-
- boolean mInBatchEditControllers;
-
- SuggestionsPopupWindow mSuggestionsPopupWindow;
- SuggestionRangeSpan mSuggestionRangeSpan;
- Runnable mShowSuggestionRunnable;
-
- final Drawable[] mCursorDrawable = new Drawable[2];
- int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
-
- Drawable mSelectHandleLeft;
- Drawable mSelectHandleRight;
- Drawable mSelectHandleCenter;
-
- // Global listener that detects changes in the global position of the TextView
- PositionListener mPositionListener;
-
- float mLastDownPositionX, mLastDownPositionY;
- Callback mCustomSelectionActionModeCallback;
-
- // Set when this TextView gained focus with some text selected. Will start selection mode.
- boolean mCreatedWithASelection;
-
- WordIterator mWordIterator;
- SpellChecker mSpellChecker;
-
- void onAttachedToWindow() {
- final ViewTreeObserver observer = getViewTreeObserver();
- // No need to create the controller.
- // The get method will add the listener on controller creation.
- if (mInsertionPointCursorController != null) {
- observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
- }
- if (mSelectionModifierCursorController != null) {
- observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
- }
- updateSpellCheckSpans(0, mText.length(), true /* create the spell checker if needed */);
- }
-
- void onDetachedFromWindow() {
- if (mError != null) {
- hideError();
- }
-
- if (mBlink != null) {
- mBlink.removeCallbacks(mBlink);
- }
-
- if (mInsertionPointCursorController != null) {
- mInsertionPointCursorController.onDetached();
- }
-
- if (mSelectionModifierCursorController != null) {
- mSelectionModifierCursorController.onDetached();
- }
-
- if (mShowSuggestionRunnable != null) {
- removeCallbacks(mShowSuggestionRunnable);
- }
-
- invalidateTextDisplayList();
-
- if (mSpellChecker != null) {
- mSpellChecker.closeSession();
- // Forces the creation of a new SpellChecker next time this window is created.
- // Will handle the cases where the settings has been changed in the meantime.
- mSpellChecker = null;
- }
-
- hideControllers();
- }
-
- void onScreenStateChanged(int screenState) {
- switch (screenState) {
- case SCREEN_STATE_ON:
- resumeBlink();
- break;
- case SCREEN_STATE_OFF:
- suspendBlink();
- break;
- }
- }
-
- private void suspendBlink() {
- if (mBlink != null) {
- mBlink.cancel();
- }
- }
-
- private void resumeBlink() {
- if (mBlink != null) {
- mBlink.uncancel();
- makeBlink();
- }
- }
-
- void adjustInputType(boolean password, boolean passwordInputType,
- boolean webPasswordInputType, boolean numberPasswordInputType) {
- // mInputType has been set from inputType, possibly modified by mInputMethod.
- // Specialize mInputType to [web]password if we have a text class and the original input
- // type was a password.
- if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
- if (password || passwordInputType) {
- mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
- | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
- }
- if (webPasswordInputType) {
- mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
- | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
- }
- } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
- if (numberPasswordInputType) {
- mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
- | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
- }
- }
- }
-
- void setFrame() {
- if (mErrorPopup != null) {
- TextView tv = (TextView) mErrorPopup.getContentView();
- chooseSize(mErrorPopup, mError, tv);
- mErrorPopup.update(TextView.this, getErrorX(), getErrorY(),
- mErrorPopup.getWidth(), mErrorPopup.getHeight());
- }
- }
-
- void onFocusChanged(boolean focused, int direction) {
- mShowCursor = SystemClock.uptimeMillis();
- ensureEndedBatchEdit();
-
- if (focused) {
- int selStart = getSelectionStart();
- int selEnd = getSelectionEnd();
-
- // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
- // mode for these, unless there was a specific selection already started.
- final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
- selEnd == mText.length();
-
- mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
-
- if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
- // If a tap was used to give focus to that view, move cursor at tap position.
- // Has to be done before onTakeFocus, which can be overloaded.
- final int lastTapPosition = getLastTapPosition();
- if (lastTapPosition >= 0) {
- Selection.setSelection((Spannable) mText, lastTapPosition);
- }
-
- // Note this may have to be moved out of the Editor class
- if (mMovement != null) {
- mMovement.onTakeFocus(TextView.this, (Spannable) mText, direction);
- }
-
- // The DecorView does not have focus when the 'Done' ExtractEditText button is
- // pressed. Since it is the ViewAncestor's mView, it requests focus before
- // ExtractEditText clears focus, which gives focus to the ExtractEditText.
- // This special case ensure that we keep current selection in that case.
- // It would be better to know why the DecorView does not have focus at that time.
- if (((TextView.this instanceof ExtractEditText) || mSelectionMoved) &&
- selStart >= 0 && selEnd >= 0) {
- /*
- * Someone intentionally set the selection, so let them
- * do whatever it is that they wanted to do instead of
- * the default on-focus behavior. We reset the selection
- * here instead of just skipping the onTakeFocus() call
- * because some movement methods do something other than
- * just setting the selection in theirs and we still
- * need to go through that path.
- */
- Selection.setSelection((Spannable) mText, selStart, selEnd);
- }
-
- if (mSelectAllOnFocus) {
- selectAll();
- }
-
- mTouchFocusSelected = true;
- }
-
- mFrozenWithFocus = false;
- mSelectionMoved = false;
-
- if (mError != null) {
- showError();
- }
-
- makeBlink();
- } else {
- if (mError != null) {
- hideError();
- }
- // Don't leave us in the middle of a batch edit.
- onEndBatchEdit();
-
- if (TextView.this instanceof ExtractEditText) {
- // terminateTextSelectionMode removes selection, which we want to keep when
- // ExtractEditText goes out of focus.
- final int selStart = getSelectionStart();
- final int selEnd = getSelectionEnd();
- hideControllers();
- Selection.setSelection((Spannable) mText, selStart, selEnd);
- } else {
- hideControllers();
- downgradeEasyCorrectionSpans();
- }
-
- // No need to create the controller
- if (mSelectionModifierCursorController != null) {
- mSelectionModifierCursorController.resetTouchOffsets();
- }
- }
- }
-
- void sendOnTextChanged(int start, int after) {
- updateSpellCheckSpans(start, start + after, false);
-
- // Hide the controllers as soon as text is modified (typing, procedural...)
- // We do not hide the span controllers, since they can be added when a new text is
- // inserted into the text view (voice IME).
- hideCursorControllers();
- }
-
- private int getLastTapPosition() {
- // No need to create the controller at that point, no last tap position saved
- if (mSelectionModifierCursorController != null) {
- int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
- if (lastTapPosition >= 0) {
- // Safety check, should not be possible.
- if (lastTapPosition > mText.length()) {
- Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
- + mText.length() + ")");
- lastTapPosition = mText.length();
- }
- return lastTapPosition;
- }
- }
-
- return -1;
- }
-
- void onWindowFocusChanged(boolean hasWindowFocus) {
- if (hasWindowFocus) {
- if (mBlink != null) {
- mBlink.uncancel();
- makeBlink();
- }
- } else {
- if (mBlink != null) {
- mBlink.cancel();
- }
- if (mInputContentType != null) {
- mInputContentType.enterDown = false;
- }
- // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
- hideControllers();
- if (mSuggestionsPopupWindow != null) {
- mSuggestionsPopupWindow.onParentLostFocus();
- }
-
- // Don't leave us in the middle of a batch edit.
- onEndBatchEdit();
- }
- }
-
- void onTouchEvent(MotionEvent event) {
- if (hasSelectionController()) {
- getSelectionController().onTouchEvent(event);
- }
-
- if (mShowSuggestionRunnable != null) {
- removeCallbacks(mShowSuggestionRunnable);
- mShowSuggestionRunnable = null;
- }
-
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mLastDownPositionX = event.getX();
- mLastDownPositionY = event.getY();
-
- // Reset this state; it will be re-set if super.onTouchEvent
- // causes focus to move to the view.
- mTouchFocusSelected = false;
- mIgnoreActionUpEvent = false;
- }
- }
-
- void onDraw(Canvas canvas, Layout layout, Path highlight, int cursorOffsetVertical) {
- final int selectionStart = getSelectionStart();
- final int selectionEnd = getSelectionEnd();
-
- final InputMethodState ims = mInputMethodState;
- if (ims != null && ims.mBatchEditNesting == 0) {
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) {
- if (imm.isActive(TextView.this)) {
- boolean reported = false;
- if (ims.mContentChanged || ims.mSelectionModeChanged) {
- // We are in extract mode and the content has changed
- // in some way... just report complete new text to the
- // input method.
- reported = reportExtractedText();
- }
- if (!reported && highlight != null) {
- int candStart = -1;
- int candEnd = -1;
- if (mText instanceof Spannable) {
- Spannable sp = (Spannable)mText;
- candStart = EditableInputConnection.getComposingSpanStart(sp);
- candEnd = EditableInputConnection.getComposingSpanEnd(sp);
- }
- imm.updateSelection(TextView.this,
- selectionStart, selectionEnd, candStart, candEnd);
- }
- }
-
- if (imm.isWatchingCursor(TextView.this) && highlight != null) {
- highlight.computeBounds(ims.mTmpRectF, true);
- ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
-
- canvas.getMatrix().mapPoints(ims.mTmpOffset);
- ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
-
- ims.mTmpRectF.offset(0, cursorOffsetVertical);
-
- ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
- (int)(ims.mTmpRectF.top + 0.5),
- (int)(ims.mTmpRectF.right + 0.5),
- (int)(ims.mTmpRectF.bottom + 0.5));
-
- imm.updateCursor(TextView.this,
- ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
- ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
- }
- }
- }
-
- if (mCorrectionHighlighter != null) {
- mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
- }
-
- if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
- drawCursor(canvas, cursorOffsetVertical);
- // Rely on the drawable entirely, do not draw the cursor line.
- // Has to be done after the IMM related code above which relies on the highlight.
- highlight = null;
- }
-
- if (canHaveDisplayList() && canvas.isHardwareAccelerated()) {
- drawHardwareAccelerated(canvas, layout, highlight, cursorOffsetVertical);
- } else {
- layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
- }
-
- if (mMarquee != null && mMarquee.shouldDrawGhost()) {
- canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
- layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
- }
- }
-
- private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
- int cursorOffsetVertical) {
- final int width = mRight - mLeft;
- final int height = mBottom - mTop;
-
- final long lineRange = layout.getLineRangeForDraw(canvas);
- int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
- int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
- if (lastLine < 0) return;
-
- layout.drawBackground(canvas, highlight, mHighlightPaint, cursorOffsetVertical,
- firstLine, lastLine);
-
- if (layout instanceof DynamicLayout) {
- if (mTextDisplayLists == null) {
- mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
- }
-
- DynamicLayout dynamicLayout = (DynamicLayout) layout;
- int[] blockEnds = dynamicLayout.getBlockEnds();
- int[] blockIndices = dynamicLayout.getBlockIndices();
- final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
-
- canvas.translate(mScrollX, mScrollY);
- int endOfPreviousBlock = -1;
- int searchStartIndex = 0;
- for (int i = 0; i < numberOfBlocks; i++) {
- int blockEnd = blockEnds[i];
- int blockIndex = blockIndices[i];
-
- final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
- if (blockIsInvalid) {
- blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
- searchStartIndex);
- // Dynamic layout internal block indices structure is updated from Editor
- blockIndices[i] = blockIndex;
- searchStartIndex = blockIndex + 1;
- }
-
- DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
- if (blockDisplayList == null) {
- blockDisplayList = mTextDisplayLists[blockIndex] =
- getHardwareRenderer().createDisplayList("Text " + blockIndex);
- } else {
- if (blockIsInvalid) blockDisplayList.invalidate();
- }
-
- if (!blockDisplayList.isValid()) {
- final HardwareCanvas hardwareCanvas = blockDisplayList.start();
- try {
- hardwareCanvas.setViewport(width, height);
- // The dirty rect should always be null for a display list
- hardwareCanvas.onPreDraw(null);
- hardwareCanvas.translate(-mScrollX, -mScrollY);
- layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd);
- hardwareCanvas.translate(mScrollX, mScrollY);
- } finally {
- hardwareCanvas.onPostDraw();
- blockDisplayList.end();
- if (USE_DISPLAY_LIST_PROPERTIES) {
- blockDisplayList.setLeftTopRightBottom(0, 0, width, height);
- }
- }
- }
-
- ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null,
- DisplayList.FLAG_CLIP_CHILDREN);
- endOfPreviousBlock = blockEnd;
- }
- canvas.translate(-mScrollX, -mScrollY);
- } else {
- // Fallback on the layout method (a BoringLayout is used when the text is empty)
- layout.drawText(canvas, firstLine, lastLine);
- }
- }
-
- private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
- int searchStartIndex) {
- int length = mTextDisplayLists.length;
- for (int i = searchStartIndex; i < length; i++) {
- boolean blockIndexFound = false;
- for (int j = 0; j < numberOfBlocks; j++) {
- if (blockIndices[j] == i) {
- blockIndexFound = true;
- break;
- }
- }
- if (blockIndexFound) continue;
- return i;
- }
-
- // No available index found, the pool has to grow
- int newSize = ArrayUtils.idealIntArraySize(length + 1);
- DisplayList[] displayLists = new DisplayList[newSize];
- System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
- mTextDisplayLists = displayLists;
- return length;
- }
-
- private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
- final boolean translate = cursorOffsetVertical != 0;
- if (translate) canvas.translate(0, cursorOffsetVertical);
- for (int i = 0; i < getEditor().mCursorCount; i++) {
- mCursorDrawable[i].draw(canvas);
- }
- if (translate) canvas.translate(0, -cursorOffsetVertical);
- }
-
- private void invalidateTextDisplayList() {
- if (mTextDisplayLists != null) {
- for (int i = 0; i < mTextDisplayLists.length; i++) {
- if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
- }
- }
- }
-
- private void updateCursorsPositions() {
- if (mCursorDrawableRes == 0) {
- mCursorCount = 0;
- return;
- }
-
- final int offset = getSelectionStart();
- final int line = mLayout.getLineForOffset(offset);
- final int top = mLayout.getLineTop(line);
- final int bottom = mLayout.getLineTop(line + 1);
-
- mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
-
- int middle = bottom;
- if (mCursorCount == 2) {
- // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
- middle = (top + bottom) >> 1;
- }
-
- updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
-
- if (mCursorCount == 2) {
- updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
- }
- }
-
- private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
- if (mCursorDrawable[cursorIndex] == null)
- mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
-
- if (mTempRect == null) mTempRect = new Rect();
- mCursorDrawable[cursorIndex].getPadding(mTempRect);
- final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
- horizontal = Math.max(0.5f, horizontal - 0.5f);
- final int left = (int) (horizontal) - mTempRect.left;
- mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
- bottom + mTempRect.bottom);
- }
}
}
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index aca1fa2..6e36fdb 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -35,5 +35,7 @@
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..118e541 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -47,4 +47,6 @@
void setSystemUiVisibility(int vis);
void setHardKeyboardEnabled(boolean enabled);
void toggleRecentApps();
+ void preloadRecentApps();
+ void cancelPreloadRecentApps();
}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 5a7d519..93f90f6 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -438,10 +438,9 @@
* Calls back SetupFaceLock to delete the temporary gallery file
*/
public void deleteTempGallery() {
- Intent intent = new Intent().setClassName("com.android.facelock",
- "com.android.facelock.SetupFaceLock");
+ Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY");
intent.putExtra("deleteTempGallery", true);
- mContext.startActivity(intent);
+ mContext.sendBroadcast(intent);
}
/**
@@ -449,10 +448,9 @@
*/
void deleteGallery() {
if(usingBiometricWeak()) {
- Intent intent = new Intent().setClassName("com.android.facelock",
- "com.android.facelock.SetupFaceLock");
+ Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY");
intent.putExtra("deleteGallery", true);
- mContext.startActivity(intent);
+ mContext.sendBroadcast(intent);
}
}
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index 8a1c4a9..72c171c 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -49,7 +49,6 @@
status_t initialize();
status_t scheduleVsync();
- static int handleReceiveCallback(int receiveFd, int events, void* data);
protected:
virtual ~NativeDisplayEventReceiver();
@@ -59,6 +58,9 @@
sp<Looper> mLooper;
DisplayEventReceiver mReceiver;
bool mWaitingForVsync;
+
+ static int handleReceiveCallback(int receiveFd, int events, void* data);
+ bool readLastVsyncMessage(nsecs_t* outTimestamp, uint32_t* outCount);
};
@@ -100,16 +102,9 @@
ALOGV("receiver %p ~ Scheduling vsync.", this);
// Drain all pending events.
- DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
- ssize_t n;
- while ((n = mReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
- ALOGV("receiver %p ~ Drained %d events.", this, int(n));
- }
-
- if (n < 0) {
- ALOGW("Failed to drain events from display event receiver, status=%d", status_t(n));
- return status_t(n);
- }
+ nsecs_t vsyncTimestamp;
+ uint32_t vsyncCount;
+ readLastVsyncMessage(&vsyncTimestamp, &vsyncCount);
status_t status = mReceiver.requestNextVsync();
if (status) {
@@ -138,23 +133,9 @@
}
// Drain all pending events, keep the last vsync.
- nsecs_t vsyncTimestamp = -1;
- uint32_t vsyncCount = 0;
-
- DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
- ssize_t n;
- while ((n = r->mReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
- ALOGV("receiver %p ~ Read %d events.", data, int(n));
- while (n-- > 0) {
- if (buf[n].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) {
- vsyncTimestamp = buf[n].header.timestamp;
- vsyncCount = buf[n].vsync.count;
- break; // stop at last vsync in the buffer
- }
- }
- }
-
- if (vsyncTimestamp < 0) {
+ nsecs_t vsyncTimestamp;
+ uint32_t vsyncCount;
+ if (!r->readLastVsyncMessage(&vsyncTimestamp, &vsyncCount)) {
ALOGV("receiver %p ~ Woke up but there was no vsync pulse!", data);
return 1; // keep the callback, did not obtain a vsync pulse
}
@@ -179,6 +160,26 @@
return 1; // keep the callback
}
+bool NativeDisplayEventReceiver::readLastVsyncMessage(
+ nsecs_t* outTimestamp, uint32_t* outCount) {
+ DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
+ ssize_t n;
+ while ((n = mReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
+ ALOGV("receiver %p ~ Read %d events.", this, int(n));
+ while (n-- > 0) {
+ if (buf[n].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) {
+ *outTimestamp = buf[n].header.timestamp;
+ *outCount = buf[n].vsync.count;
+ return true; // stop at last vsync in the buffer
+ }
+ }
+ }
+ if (n < 0) {
+ ALOGW("Failed to get events from display event receiver, status=%d", status_t(n));
+ }
+ return false;
+}
+
static jint nativeInit(JNIEnv* env, jclass clazz, jobject receiverObj,
jobject messageQueueObj) {
diff --git a/core/jni/android_view_HardwareRenderer.cpp b/core/jni/android_view_HardwareRenderer.cpp
index 4b62169..082e726 100644
--- a/core/jni/android_view_HardwareRenderer.cpp
+++ b/core/jni/android_view_HardwareRenderer.cpp
@@ -99,10 +99,26 @@
// Tracing and debugging
// ----------------------------------------------------------------------------
-static void android_view_HardwareRenderer_beginFrame(JNIEnv* env, jobject clazz) {
- EGLDisplay dpy = eglGetCurrentDisplay();
- EGLSurface surf = eglGetCurrentSurface(EGL_DRAW);
- eglBeginFrame(dpy, surf);
+static void android_view_HardwareRenderer_beginFrame(JNIEnv* env, jobject clazz,
+ jintArray size) {
+
+ EGLDisplay display = eglGetCurrentDisplay();
+ EGLSurface surface = eglGetCurrentSurface(EGL_DRAW);
+
+ if (size) {
+ EGLint value;
+ jint* storage = env->GetIntArrayElements(size, NULL);
+
+ eglQuerySurface(display, surface, EGL_WIDTH, &value);
+ storage[0] = value;
+
+ eglQuerySurface(display, surface, EGL_HEIGHT, &value);
+ storage[1] = value;
+
+ env->ReleaseIntArrayElements(size, storage, 0);
+ }
+
+ eglBeginFrame(display, surface);
}
#endif // USE_OPENGL_RENDERER
@@ -127,15 +143,11 @@
static JNINativeMethod gMethods[] = {
#ifdef USE_OPENGL_RENDERER
- { "nIsBackBufferPreserved", "()Z",
- (void*) android_view_HardwareRenderer_isBackBufferPreserved },
- { "nPreserveBackBuffer", "()Z",
- (void*) android_view_HardwareRenderer_preserveBackBuffer },
- { "nDisableVsync", "()V",
- (void*) android_view_HardwareRenderer_disableVsync },
+ { "nIsBackBufferPreserved", "()Z", (void*) android_view_HardwareRenderer_isBackBufferPreserved },
+ { "nPreserveBackBuffer", "()Z", (void*) android_view_HardwareRenderer_preserveBackBuffer },
+ { "nDisableVsync", "()V", (void*) android_view_HardwareRenderer_disableVsync },
- { "nBeginFrame", "()V",
- (void*) android_view_HardwareRenderer_beginFrame },
+ { "nBeginFrame", "([I)V", (void*) android_view_HardwareRenderer_beginFrame },
#endif
{ "nSetupShadersDiskCache", "(Ljava/lang/String;)V",
diff --git a/core/res/res/layout/notification_template_big_picture.xml b/core/res/res/layout/notification_template_big_picture.xml
new file mode 100644
index 0000000..6eb934e
--- /dev/null
+++ b/core/res/res/layout/notification_template_big_picture.xml
@@ -0,0 +1,17 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/status_bar_latest_event_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+ <ImageView
+ android:id="@+id/big_picture"
+ android:layout_width="match_parent"
+ android:layout_height="192dp"
+ android:scaleType="centerCrop"
+ />
+ <include layout="@layout/status_bar_latest_event_content"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/notification_large_icon_height"
+ android:layout_marginTop="192dp"
+ />
+</FrameLayout>
\ No newline at end of file
diff --git a/core/res/res/layout/status_bar_latest_event_content.xml b/core/res/res/layout/status_bar_latest_event_content.xml
index ec1bc81..57c149f 100644
--- a/core/res/res/layout/status_bar_latest_event_content.xml
+++ b/core/res/res/layout/status_bar_latest_event_content.xml
@@ -1,7 +1,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="wrap_content"
>
<ImageView android:id="@+id/icon"
android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/status_bar_latest_event_content_large_icon.xml b/core/res/res/layout/status_bar_latest_event_content_large_icon.xml
index a2253b7..5f38e6a 100644
--- a/core/res/res/layout/status_bar_latest_event_content_large_icon.xml
+++ b/core/res/res/layout/status_bar_latest_event_content_large_icon.xml
@@ -6,6 +6,8 @@
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
>
<LinearLayout
android:id="@+id/line1"
@@ -44,6 +46,13 @@
android:ellipsize="marquee"
android:visibility="gone"
/>
+ <TextView android:id="@+id/big_text"
+ android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="false"
+ android:visibility="gone"
+ />
<LinearLayout
android:id="@+id/line3"
android:layout_width="match_parent"
@@ -70,7 +79,7 @@
android:gravity="center"
android:paddingLeft="8dp"
/>
- <ImageView android:id="@+id/icon"
+ <ImageView android:id="@+id/right_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
diff --git a/core/res/res/values-af/strings.xml b/core/res/res/values-af/strings.xml
index f020f5d..203c335 100644
--- a/core/res/res/values-af/strings.xml
+++ b/core/res/res/values-af/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Voer \'n PUK van 8 syfers of langer in."</string>
<string name="needPuk" msgid="919668385956251611">"Jou SIM-kaart is PUK-gesluit. Voer die PUK-kode in om dit te ontsluit."</string>
<string name="needPuk2" msgid="4526033371987193070">"Sleutel PUK2 in om SIM-kaart oop te sluit."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Inkomender beller-ID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Uitgaande beller-ID"</string>
<string name="CfMmi" msgid="5123218989141573515">"Oproepaanstuur"</string>
diff --git a/core/res/res/values-am/strings.xml b/core/res/res/values-am/strings.xml
index 0481a67..ab937c5 100644
--- a/core/res/res/values-am/strings.xml
+++ b/core/res/res/values-am/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"8 ወይም ከዛ በላይ የሆኑ ቁጥሮችንPUK ተይብ።"</string>
<string name="needPuk" msgid="919668385956251611">"SIM ካርድዎ PUK-የተቆለፈ ነው።የPUK ኮዱን በመተየብ ይክፈቱት።"</string>
<string name="needPuk2" msgid="4526033371987193070">" SIM ለመክፈት PUK2 ተይብ።"</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"የገቢ ደዋይID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"የወጪ ጥሪID"</string>
<string name="CfMmi" msgid="5123218989141573515">"ጥሪ ማስተላለፍ"</string>
diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml
index a156e64..aca8f47 100644
--- a/core/res/res/values-ar/strings.xml
+++ b/core/res/res/values-ar/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"اكتب رمز PUK مكونًا من 8 أرقام أو أكثر."</string>
<string name="needPuk" msgid="919668385956251611">"بطاقة SIM مؤمّنة بكود PUK. اكتب كود PUK لإلغاء تأمينها."</string>
<string name="needPuk2" msgid="4526033371987193070">"اكتب PUK2 لإلغاء تأمين بطاقة SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"معرف المتصل الوارد"</string>
<string name="ClirMmi" msgid="7784673673446833091">"معرف المتصل الصادر"</string>
<string name="CfMmi" msgid="5123218989141573515">"إعادة توجيه الاتصال"</string>
diff --git a/core/res/res/values-be/strings.xml b/core/res/res/values-be/strings.xml
index 0178764..4d304f1 100644
--- a/core/res/res/values-be/strings.xml
+++ b/core/res/res/values-be/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Увядзіце PUK з 8 лічбаў ці больш."</string>
<string name="needPuk" msgid="919668385956251611">"Ваша SIM-карта заблакавана PUK-кодам. Увядзіце PUK, каб разблакаваць карту."</string>
<string name="needPuk2" msgid="4526033371987193070">"Увядзіце PUK2 для разблакавання SIM-карты."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Ідэнтыфікатар АВН"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Ідэнтыфікатар АВН"</string>
<string name="CfMmi" msgid="5123218989141573515">"Пераадрасацыя выкліку"</string>
diff --git a/core/res/res/values-bg/strings.xml b/core/res/res/values-bg/strings.xml
index 479e118..b057cef 100644
--- a/core/res/res/values-bg/strings.xml
+++ b/core/res/res/values-bg/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Въведете PUK код с поне осем цифри."</string>
<string name="needPuk" msgid="919668385956251611">"SIM картата ви е заключена с PUK. Въведете PUK кода, за да я отключите."</string>
<string name="needPuk2" msgid="4526033371987193070">"Въведете PUK2, за да отблокирате SIM картата."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Идентификация на вх. обаждания"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Идентификация на изходящите повиквания"</string>
<string name="CfMmi" msgid="5123218989141573515">"Пренасочване на повиквания"</string>
diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml
index 64d0f87..61c3cf0 100644
--- a/core/res/res/values-ca/strings.xml
+++ b/core/res/res/values-ca/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Introdueix un PUK compost com a mínim de 8 nombres."</string>
<string name="needPuk" msgid="919668385956251611">"La targeta SIM està bloquejada pel PUK. Escriviu el codi PUK per desbloquejar-la."</string>
<string name="needPuk2" msgid="4526033371987193070">"Escriviu el PUK2 per desbloquejar la targeta SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Identificació de trucada entrant"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Identificació de trucada de sortida"</string>
<string name="CfMmi" msgid="5123218989141573515">"Desviació de trucades"</string>
diff --git a/core/res/res/values-cs/strings.xml b/core/res/res/values-cs/strings.xml
index d2a4cc1..ec640f7 100644
--- a/core/res/res/values-cs/strings.xml
+++ b/core/res/res/values-cs/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Zadejte osmimístný nebo delší kód PUK."</string>
<string name="needPuk" msgid="919668385956251611">"Karta SIM je blokována pomocí kódu PUK. Odblokujete ji zadáním kódu PUK."</string>
<string name="needPuk2" msgid="4526033371987193070">"Chcete-li odblokovat kartu SIM, zadejte kód PUK2."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Příchozí identifikace volajícího"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Odchozí identifikace volajícího"</string>
<string name="CfMmi" msgid="5123218989141573515">"Přesměrování hovorů"</string>
diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml
index b139e5e..d72364b 100644
--- a/core/res/res/values-da/strings.xml
+++ b/core/res/res/values-da/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Angiv en PUK-kode på 8 eller flere cifre."</string>
<string name="needPuk" msgid="919668385956251611">"Dit SIM-kort er låst med PUK-koden. Indtast PUK-koden for at låse den op."</string>
<string name="needPuk2" msgid="4526033371987193070">"Indtast PUK2-koden for at låse op for SIM-kortet."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Indgående opkalds-id"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Udgående opkalds-id"</string>
<string name="CfMmi" msgid="5123218989141573515">"Viderestilling af opkald"</string>
diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml
index baf3e40..defd850 100644
--- a/core/res/res/values-de/strings.xml
+++ b/core/res/res/values-de/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Geben Sie eine mindestens achtstellige PUK ein."</string>
<string name="needPuk" msgid="919668385956251611">"Ihre SIM-Karte ist mit einem PUK gesperrt. Geben Sie zum Entsperren den PUK-Code ein."</string>
<string name="needPuk2" msgid="4526033371987193070">"Geben Sie zum Entsperren der SIM-Karte den PUK2 ein."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Anrufer-ID für eingehenden Anruf"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Anrufer-ID für ausgehenden Anruf"</string>
<string name="CfMmi" msgid="5123218989141573515">"Rufweiterleitung"</string>
diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml
index ad840e3..71547ce 100644
--- a/core/res/res/values-el/strings.xml
+++ b/core/res/res/values-el/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Πληκτρολογήστε έναν κωδικό PUK με 8 αριθμούς ή περισσότερους."</string>
<string name="needPuk" msgid="919668385956251611">"Η κάρτα SIM έχει κλειδωθεί με κωδικό PUK. Πληκτρολογήστε τον κωδικό PUK για να την ξεκλειδώσετε."</string>
<string name="needPuk2" msgid="4526033371987193070">"Πληκτρολογήστε τον κωδικό PUK2 για την κατάργηση αποκλεισμού της κάρτας SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Εισερχόμενη αναγνώριση κλήσης"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Εξερχόμενη αναγνώριση κλήσης"</string>
<string name="CfMmi" msgid="5123218989141573515">"Προώθηση κλήσεων"</string>
diff --git a/core/res/res/values-en-rGB/strings.xml b/core/res/res/values-en-rGB/strings.xml
index de92665..b070e02 100644
--- a/core/res/res/values-en-rGB/strings.xml
+++ b/core/res/res/values-en-rGB/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Type a PUK that is 8 numbers or longer."</string>
<string name="needPuk" msgid="919668385956251611">"Your SIM card is PUK-locked. Type the PUK code to unlock it."</string>
<string name="needPuk2" msgid="4526033371987193070">"Type PUK2 to unblock SIM card."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Incoming Caller ID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Outgoing Caller ID"</string>
<string name="CfMmi" msgid="5123218989141573515">"Call forwarding"</string>
diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml
index 8761ca9..8de6b8c0 100644
--- a/core/res/res/values-es-rUS/strings.xml
+++ b/core/res/res/values-es-rUS/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Ingresa un código PUK de ocho números o más."</string>
<string name="needPuk" msgid="919668385956251611">"Tu tarjeta SIM está bloqueada con PUK. Escribe el código PUK para desbloquearla."</string>
<string name="needPuk2" msgid="4526033371987193070">"Escribir PUK2 para desbloquear la tarjeta SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Identificador de llamadas entrantes"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Identificador de llamadas salientes"</string>
<string name="CfMmi" msgid="5123218989141573515">"Desvío de llamadas"</string>
diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml
index 9ede6a0..b76053f 100644
--- a/core/res/res/values-es/strings.xml
+++ b/core/res/res/values-es/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Escribe un código PUK de ocho caracteres o más."</string>
<string name="needPuk" msgid="919668385956251611">"La tarjeta SIM está bloqueada con el código PUK. Introduce el código PUK para desbloquearla."</string>
<string name="needPuk2" msgid="4526033371987193070">"Introduce el código PUK2 para desbloquear la tarjeta SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID de emisor de llamada entrante"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID de emisor de llamada saliente"</string>
<string name="CfMmi" msgid="5123218989141573515">"Desvío de llamada"</string>
diff --git a/core/res/res/values-et/strings.xml b/core/res/res/values-et/strings.xml
index b68944e..38c9251 100644
--- a/core/res/res/values-et/strings.xml
+++ b/core/res/res/values-et/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Sisestage 8- või enamanumbriline PUK-kood."</string>
<string name="needPuk" msgid="919668385956251611">"SIM-kaart on PUK-lukustatud. Avamiseks sisestage PUK-kood."</string>
<string name="needPuk2" msgid="4526033371987193070">"Sisestage SIM-kaardi blokeeringu tühistamiseks PUK2-kood."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Sissetuleva kõne helistaja ID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Väljuva kõne helistaja ID"</string>
<string name="CfMmi" msgid="5123218989141573515">"Kõnede suunamine"</string>
diff --git a/core/res/res/values-fa/strings.xml b/core/res/res/values-fa/strings.xml
index 7ee47e7..5a50a8f 100644
--- a/core/res/res/values-fa/strings.xml
+++ b/core/res/res/values-fa/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"یک PUK با 8 رقم یا بیشتر تایپ کنید."</string>
<string name="needPuk" msgid="919668385956251611">"سیم کارت شما با PUK قفل شده است. کد PUK را برای بازگشایی آن بنویسید."</string>
<string name="needPuk2" msgid="4526033371987193070">"PUK2 را برای بازگشایی قفل سیم کارت بنویسید."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"شناسه تماس گیرنده ورودی"</string>
<string name="ClirMmi" msgid="7784673673446833091">"شناسه تماس گیرنده خروجی"</string>
<string name="CfMmi" msgid="5123218989141573515">"هدایت تماس"</string>
diff --git a/core/res/res/values-fi/strings.xml b/core/res/res/values-fi/strings.xml
index b1009a5..6d849dd 100644
--- a/core/res/res/values-fi/strings.xml
+++ b/core/res/res/values-fi/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Kirjoita vähintään 8 numeron pituinen PUK-koodi."</string>
<string name="needPuk" msgid="919668385956251611">"SIM-korttisi on PUK-lukittu. Poista lukitus antamalla PUK-koodi."</string>
<string name="needPuk2" msgid="4526033371987193070">"Pura SIM-kortin esto antamalla PUK2-koodi."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Soittajan tunnus"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Soittajan tunnus"</string>
<string name="CfMmi" msgid="5123218989141573515">"Soitonsiirto"</string>
diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml
index d044a70..548d66b 100644
--- a/core/res/res/values-fr/strings.xml
+++ b/core/res/res/values-fr/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Saisissez un code PUK comportant au moins huit chiffres."</string>
<string name="needPuk" msgid="919668385956251611">"Votre carte SIM est verrouillée par clé PUK. Saisissez la clé PUK pour la déverrouiller."</string>
<string name="needPuk2" msgid="4526033371987193070">"Saisissez la clé PUK2 pour débloquer la carte SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Numéro de l\'appelant (entrant)"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Numéro de l\'appelant (sortant)"</string>
<string name="CfMmi" msgid="5123218989141573515">"Transfert d\'appel"</string>
diff --git a/core/res/res/values-hi/strings.xml b/core/res/res/values-hi/strings.xml
index bd8863f..be85378 100644
--- a/core/res/res/values-hi/strings.xml
+++ b/core/res/res/values-hi/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"ऐसा PUK लिखें जो 8 अंकों या अधिक का हो."</string>
<string name="needPuk" msgid="919668385956251611">"आपका सिम कार्ड PUK लॉक किया गया है. इसे अनलॉक करने के लिए PUK कोड लिखें."</string>
<string name="needPuk2" msgid="4526033371987193070">"सिम कार्ड अनब्लॉक करने के लिए PUK2 लिखें."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"इनकमिंग कॉलर ID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"आउटगोइंग कॉलर ID"</string>
<string name="CfMmi" msgid="5123218989141573515">"कॉल अग्रेषण"</string>
diff --git a/core/res/res/values-hr/strings.xml b/core/res/res/values-hr/strings.xml
index 749012b..a30a5e0 100644
--- a/core/res/res/values-hr/strings.xml
+++ b/core/res/res/values-hr/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Upišite PUK koji se sastoji od barem 8 brojeva."</string>
<string name="needPuk" msgid="919668385956251611">"Vaša je SIM kartica zaključana PUK-om. Unesite PUK kôd da biste je otključali."</string>
<string name="needPuk2" msgid="4526033371987193070">"Unesite PUK2 da biste odblokirali SIM karticu."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID dolaznog pozivatelja"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID izlaznog pozivatelja"</string>
<string name="CfMmi" msgid="5123218989141573515">"Preusmjeravanje poziva"</string>
diff --git a/core/res/res/values-hu/strings.xml b/core/res/res/values-hu/strings.xml
index 24a70d7..f6fd651 100644
--- a/core/res/res/values-hu/strings.xml
+++ b/core/res/res/values-hu/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"8 számjegyű vagy hosszabb PUK kódot írjon be."</string>
<string name="needPuk" msgid="919668385956251611">"A SIM-kártya le van zárva a PUK-kóddal. A feloldáshoz adja meg a PUK-kódot."</string>
<string name="needPuk2" msgid="4526033371987193070">"A SIM-kártya feloldásához adja meg a PUK2-kódot."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Beérkező hívóazonosító"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Kimenő hívóazonosító"</string>
<string name="CfMmi" msgid="5123218989141573515">"Hívásátirányítás"</string>
diff --git a/core/res/res/values-in/strings.xml b/core/res/res/values-in/strings.xml
index 55a298e..4a142083 100644
--- a/core/res/res/values-in/strings.xml
+++ b/core/res/res/values-in/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Ketik PUK yang terdiri dari 8 angka atau lebih."</string>
<string name="needPuk" msgid="919668385956251611">"Kartu SIM Anda dikunci PUK. Ketikkan kode PUK untuk membukanya."</string>
<string name="needPuk2" msgid="4526033371987193070">"Ketikkan PUK2 untuk membuka kartu SIM"</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Nomor Penelepon Masuk"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Nomor Penelepon Keluar"</string>
<string name="CfMmi" msgid="5123218989141573515">"Penerusan panggilan"</string>
diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml
index 75ed4e8..c682a85 100644
--- a/core/res/res/values-it/strings.xml
+++ b/core/res/res/values-it/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Digita un PUK formato da almeno 8 numeri."</string>
<string name="needPuk" msgid="919668385956251611">"La SIM è bloccata tramite PUK. Digita il codice PUK per sbloccarla."</string>
<string name="needPuk2" msgid="4526033371987193070">"Digita il PUK2 per sbloccare la SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID chiamante in entrata"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID chiamante in uscita"</string>
<string name="CfMmi" msgid="5123218989141573515">"Deviazione chiamate"</string>
diff --git a/core/res/res/values-iw/strings.xml b/core/res/res/values-iw/strings.xml
index f51f232..9ebc58d 100644
--- a/core/res/res/values-iw/strings.xml
+++ b/core/res/res/values-iw/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"הקלד PUK באורך 8 מספרים או יותר."</string>
<string name="needPuk" msgid="919668385956251611">"כרטיס ה-SIM נעול באמצעות PUK. הקלד את קוד PUK כדי לבטל את נעילתו."</string>
<string name="needPuk2" msgid="4526033371987193070">"הקלד PUK2 כדי לבטל את חסימת כרטיס ה-SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"זיהוי מתקשר של שיחה נכנסת"</string>
<string name="ClirMmi" msgid="7784673673446833091">"זיהוי מתקשר בשיחה יוצאת"</string>
<string name="CfMmi" msgid="5123218989141573515">"העברת שיחות"</string>
diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml
index 549c1e2..9f70bb3 100644
--- a/core/res/res/values-ja/strings.xml
+++ b/core/res/res/values-ja/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"PUKは8桁以上で入力してください。"</string>
<string name="needPuk" msgid="919668385956251611">"SIMカードはPUKでロックされています。ロックを解除するにはPUKコードを入力してください。"</string>
<string name="needPuk2" msgid="4526033371987193070">"SIMカードのロック解除のためPUK2を入力します。"</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"着信時の発信者番号"</string>
<string name="ClirMmi" msgid="7784673673446833091">"発信者番号"</string>
<string name="CfMmi" msgid="5123218989141573515">"着信転送"</string>
diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml
index fc8e7b1..0057a5b 100644
--- a/core/res/res/values-ko/strings.xml
+++ b/core/res/res/values-ko/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"8자리 이상의 숫자 PUK를 입력합니다."</string>
<string name="needPuk" msgid="919668385956251611">"SIM 카드의 PUK가 잠겨 있습니다. 잠금해제하려면 PUK 코드를 입력하세요."</string>
<string name="needPuk2" msgid="4526033371987193070">"SIM 카드 잠금을 해제하려면 PUK2를 입력하세요."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"발신자 번호"</string>
<string name="ClirMmi" msgid="7784673673446833091">"내 발신 번호"</string>
<string name="CfMmi" msgid="5123218989141573515">"착신전환"</string>
diff --git a/core/res/res/values-lt/strings.xml b/core/res/res/values-lt/strings.xml
index 5e9d3f4..8aeb170 100644
--- a/core/res/res/values-lt/strings.xml
+++ b/core/res/res/values-lt/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Įveskite 8 skaitmenų ar ilgesnį PUK kodą."</string>
<string name="needPuk" msgid="919668385956251611">"Jūsų SIM kortelė yra užrakinta PUK kodu. Jei norite ją atrakinti, įveskite PUK kodą."</string>
<string name="needPuk2" msgid="4526033371987193070">"Įveskite PUK2 kodą, kad panaikintumėte SIM kortelės blokavimą."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Įeinančio skambintojo ID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Išeinančio skambintojo ID"</string>
<string name="CfMmi" msgid="5123218989141573515">"Skambučio peradresavimas"</string>
diff --git a/core/res/res/values-lv/strings.xml b/core/res/res/values-lv/strings.xml
index a568c3b..f434e09 100644
--- a/core/res/res/values-lv/strings.xml
+++ b/core/res/res/values-lv/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Ierakstiet PUK kodu, kas sastāv no 8 vai vairāk cipariem."</string>
<string name="needPuk" msgid="919668385956251611">"SIM karte ir bloķēta ar PUK kodu. Ierakstiet PUK kodu, lai to atbloķētu."</string>
<string name="needPuk2" msgid="4526033371987193070">"Ierakstiet PUK2 kodu, lai atbloķētu SIM karti."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Ienākošā zvana zvanītāja ID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Izejošā zvana zvanītāja ID"</string>
<string name="CfMmi" msgid="5123218989141573515">"Zvanu pāradresācija"</string>
diff --git a/core/res/res/values-ms/strings.xml b/core/res/res/values-ms/strings.xml
index 363832d..a2dd90c 100644
--- a/core/res/res/values-ms/strings.xml
+++ b/core/res/res/values-ms/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Taipkan PUK yang mempunyai 8 nombor atau lebih panjang."</string>
<string name="needPuk" msgid="919668385956251611">"Kad SIM anda dikunci PUK. Taipkan kod PUK untuk membuka kuncinya."</string>
<string name="needPuk2" msgid="4526033371987193070">"Taipkan PUK2 untuk menyahsekat kad SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID Pemanggil Masuk"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID Pemanggil Keluar"</string>
<string name="CfMmi" msgid="5123218989141573515">"Pemajuan panggilan"</string>
diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml
index 384fdac..b3ea39ff 100644
--- a/core/res/res/values-nb/strings.xml
+++ b/core/res/res/values-nb/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Skriv inn en PUK-kode på åtte tall eller mer."</string>
<string name="needPuk" msgid="919668385956251611">"SIM-kortet ditt er PUK-låst. Skriv inn PUK-koden for å låse det opp."</string>
<string name="needPuk2" msgid="4526033371987193070">"Skriv inn PUK2 for å låse opp SIM-kortet."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Inngående nummervisning"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Utgående nummervisning"</string>
<string name="CfMmi" msgid="5123218989141573515">"Viderekobling"</string>
diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml
index 030222d..5698f03 100644
--- a/core/res/res/values-nl/strings.xml
+++ b/core/res/res/values-nl/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Typ een PUK-code die 8 cijfers of langer is."</string>
<string name="needPuk" msgid="919668385956251611">"Uw SIM-kaart is vergrendeld met de PUK-code. Typ de PUK-code om te ontgrendelen."</string>
<string name="needPuk2" msgid="4526033371987193070">"Voer de PUK2-code in om de SIM-kaart te ontgrendelen."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Inkomende beller-id"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Uitgaande beller-id"</string>
<string name="CfMmi" msgid="5123218989141573515">"Oproep doorschakelen"</string>
diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml
index 9e4cbf3..da2f78d 100644
--- a/core/res/res/values-pl/strings.xml
+++ b/core/res/res/values-pl/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Wpisz kod PUK składający się z co najmniej 8 cyfr."</string>
<string name="needPuk" msgid="919668385956251611">"Karta SIM jest zablokowana kodem PUK. Wprowadź kod PUK, aby odblokować kartę."</string>
<string name="needPuk2" msgid="4526033371987193070">"Wprowadź kod PUK2, aby odblokować kartę SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Identyfikator rozmówcy przy połączeniach przychodzących"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Identyfikator rozmówcy przy połączeniach wychodzących"</string>
<string name="CfMmi" msgid="5123218989141573515">"Przekazywanie połączeń"</string>
diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml
index abe1454..ec43491 100644
--- a/core/res/res/values-pt-rPT/strings.xml
+++ b/core/res/res/values-pt-rPT/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Introduza um PUK que tenha 8 ou mais algarismos."</string>
<string name="needPuk" msgid="919668385956251611">"O seu cartão SIM está bloqueado com PUK. Introduza o código PUK para desbloqueá-lo."</string>
<string name="needPuk2" msgid="4526033371987193070">"Introduza o PUK2 para desbloquear o cartão SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID do Autor da Chamada"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID do autor da chamada efetuada"</string>
<string name="CfMmi" msgid="5123218989141573515">"Encaminhamento de chamadas"</string>
diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml
index 3ae566c..53e2a96 100644
--- a/core/res/res/values-pt/strings.xml
+++ b/core/res/res/values-pt/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Digite um PUK com oito números ou mais."</string>
<string name="needPuk" msgid="919668385956251611">"O seu cartão SIM está bloqueado por um PUK. Digite o código PUK para desbloqueá-lo."</string>
<string name="needPuk2" msgid="4526033371987193070">"Digite o PUK2 para desbloquear o cartão SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID do chamador de entrada"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID do chamador de saída"</string>
<string name="CfMmi" msgid="5123218989141573515">"Encaminhamento de chamada"</string>
diff --git a/core/res/res/values-rm/strings.xml b/core/res/res/values-rm/strings.xml
index fc6a510..5f0561b 100644
--- a/core/res/res/values-rm/strings.xml
+++ b/core/res/res/values-rm/strings.xml
@@ -57,6 +57,10 @@
<skip />
<string name="needPuk" msgid="919668385956251611">"Vossa carta SIM è bloccada cun in PUK. Endatai il PUK per debloccar ella."</string>
<string name="needPuk2" msgid="4526033371987193070">"Endatai il PUK2 per debloccar la carta SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Numer dal telefonader (entrant)"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID dal telefonader per cloms sortints"</string>
<string name="CfMmi" msgid="5123218989141573515">"Renviament da clom"</string>
diff --git a/core/res/res/values-ro/strings.xml b/core/res/res/values-ro/strings.xml
index d443d1d..11199b0 100644
--- a/core/res/res/values-ro/strings.xml
+++ b/core/res/res/values-ro/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Introduceţi un cod PUK care să aibă 8 cifre sau mai mult."</string>
<string name="needPuk" msgid="919668385956251611">"Cardul SIM este blocat cu codul PUK. Introduceţi codul PUK pentru a-l debloca."</string>
<string name="needPuk2" msgid="4526033371987193070">"Introduceţi codul PUK2 pentru a debloca cardul SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID apelant de primire"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID apelant"</string>
<string name="CfMmi" msgid="5123218989141573515">"Redirecţionarea apelurilor"</string>
diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml
index 5ed4c2e..d4adfb4 100644
--- a/core/res/res/values-ru/strings.xml
+++ b/core/res/res/values-ru/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Введите PUK-код из 8 или более цифр."</string>
<string name="needPuk" msgid="919668385956251611">"SIM-карта заблокирована с помощью кода PUK. Для разблокировки введите код PUK."</string>
<string name="needPuk2" msgid="4526033371987193070">"Для разблокировки SIM-карты введите PUK2."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Идентификация вызывающего абонента"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Идентификация звонящего абонента"</string>
<string name="CfMmi" msgid="5123218989141573515">"Переадресация вызова"</string>
diff --git a/core/res/res/values-sk/strings.xml b/core/res/res/values-sk/strings.xml
index 2e97b82..bad739f9 100644
--- a/core/res/res/values-sk/strings.xml
+++ b/core/res/res/values-sk/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Zadajte kód PUK, ktorý má 8 alebo viac čísel."</string>
<string name="needPuk" msgid="919668385956251611">"Karta SIM je uzamknutá pomocou kódu PUK. Odomknite ju zadaním kódu PUK."</string>
<string name="needPuk2" msgid="4526033371987193070">"Ak chcete odblokovať kartu SIM, zadajte kód PUK2."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Prichádzajúca identifikácia volajúceho"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Odchádzajúca identifikácia volajúceho"</string>
<string name="CfMmi" msgid="5123218989141573515">"Presmerovanie hovorov"</string>
diff --git a/core/res/res/values-sl/strings.xml b/core/res/res/values-sl/strings.xml
index f66cabb..767305c 100644
--- a/core/res/res/values-sl/strings.xml
+++ b/core/res/res/values-sl/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Vnesite 8- ali več mestni PUK."</string>
<string name="needPuk" msgid="919668385956251611">"Kartica SIM je zaklenjena s kodo PUK. Če jo želite odkleniti, vnesite kodo PUK."</string>
<string name="needPuk2" msgid="4526033371987193070">"Če želite odstraniti blokiranje kartice SIM, vnesite PUK2."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"ID dohodnega klicatelja"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID odhodnega klicatelja"</string>
<string name="CfMmi" msgid="5123218989141573515">"Preusmerjanje klicev"</string>
diff --git a/core/res/res/values-sr/strings.xml b/core/res/res/values-sr/strings.xml
index 66c0561..d03abd7 100644
--- a/core/res/res/values-sr/strings.xml
+++ b/core/res/res/values-sr/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Унесите PUK који се састоји од 8 цифара или више."</string>
<string name="needPuk" msgid="919668385956251611">"SIM картица је закључана PUK кодом. Унесите PUK кôд да бисте је откључали."</string>
<string name="needPuk2" msgid="4526033371987193070">"Унесите PUK2 да бисте деблокирали SIM картицу."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Долазни ИД позиваоца"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Одлазни ИД позиваоца"</string>
<string name="CfMmi" msgid="5123218989141573515">"Преусмеравање позива"</string>
diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml
index 6559fc5..ddaa30a 100644
--- a/core/res/res/values-sv/strings.xml
+++ b/core/res/res/values-sv/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Ange en PUK-kod med minst 8 siffror."</string>
<string name="needPuk" msgid="919668385956251611">"Ditt SIM-kort är PUK-låst. Ange PUK-koden om du vill låsa upp det."</string>
<string name="needPuk2" msgid="4526033371987193070">"Ange PUK2-koden för att häva spärren av SIM-kortet."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Nummerpresentatör för inkommande samtal"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Nummerpresentatör för utgående samtal"</string>
<string name="CfMmi" msgid="5123218989141573515">"Vidarebefordra samtal"</string>
diff --git a/core/res/res/values-sw/strings.xml b/core/res/res/values-sw/strings.xml
index 39d2aba..0424964 100644
--- a/core/res/res/values-sw/strings.xml
+++ b/core/res/res/values-sw/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Andika PUK ambayo ina urefu wa nambari 8 au zaidi."</string>
<string name="needPuk" msgid="919668385956251611">"Kadi yako ya SIM imefungwa na PUK. Anika msimbo wa PUK ili kuifungua."</string>
<string name="needPuk2" msgid="4526033371987193070">"Chapisha PUK2 ili kufungua SIM kadi."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Kitambulisho cha Mpigaji wa Simu Inayoingia"</string>
<string name="ClirMmi" msgid="7784673673446833091">"ID ya Mpigaji simu Inayotoka nje"</string>
<string name="CfMmi" msgid="5123218989141573515">"Kusambaza simu"</string>
diff --git a/core/res/res/values-th/strings.xml b/core/res/res/values-th/strings.xml
index 4b59923..7197f90 100644
--- a/core/res/res/values-th/strings.xml
+++ b/core/res/res/values-th/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"พิมพ์รหัส PUK ซึ่งต้องเป็นตัวเลขอย่างน้อย 8 หลัก"</string>
<string name="needPuk" msgid="919668385956251611">"ซิมการ์ดของคุณถูกล็อกด้วย PUK พิมพ์รหัส PUK เพื่อปลดล็อก"</string>
<string name="needPuk2" msgid="4526033371987193070">"พิมพ์ PUK2 เพื่อยกเลิกการปิดกั้นซิมการ์ด"</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"หมายเลขผู้โทรเข้า"</string>
<string name="ClirMmi" msgid="7784673673446833091">"หมายเลขผู้โทรออก"</string>
<string name="CfMmi" msgid="5123218989141573515">"การโอนสาย"</string>
diff --git a/core/res/res/values-tl/strings.xml b/core/res/res/values-tl/strings.xml
index dc22a08..ff2e7ea 100644
--- a/core/res/res/values-tl/strings.xml
+++ b/core/res/res/values-tl/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Mag-type ng PUK na may 8 numbero o mas mahaba."</string>
<string name="needPuk" msgid="919668385956251611">"Na-PUK-lock ang iyong SIM card. I-type ang PUK code upang i-unlock ito."</string>
<string name="needPuk2" msgid="4526033371987193070">"I-type ang PUK2 upang i-unblock ang SIM card."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Papasok na Caller ID"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Papalabas na Caller ID"</string>
<string name="CfMmi" msgid="5123218989141573515">"Pagpapasa ng tawag"</string>
diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml
index 419ae96..c450761 100644
--- a/core/res/res/values-tr/strings.xml
+++ b/core/res/res/values-tr/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"8 veya daha uzun basamaklı bir PUK kodu yazın."</string>
<string name="needPuk" msgid="919668385956251611">"SIM kartınızın PUK kilidi devrede. Kilidi açmak için PUK kodunu yazın."</string>
<string name="needPuk2" msgid="4526033371987193070">"Engellenen SIM kartı açmak için PUK2 kodunu yazın."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Gelen Çağrı Kimliği"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Giden Çağrı Kimliği"</string>
<string name="CfMmi" msgid="5123218989141573515">"Çağrı yönlendirme"</string>
diff --git a/core/res/res/values-uk/strings.xml b/core/res/res/values-uk/strings.xml
index 13311f1..2afb95f 100644
--- a/core/res/res/values-uk/strings.xml
+++ b/core/res/res/values-uk/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Введіть PUK-код із 8 або більше цифр."</string>
<string name="needPuk" msgid="919668385956251611">"SIM-карта заблок. PUK-кодом. Введіть PUK-код, щоб її розблок."</string>
<string name="needPuk2" msgid="4526033371987193070">"Введ. PUK2, щоб розбл. SIM-карту."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Вхідн. ід. абонента"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Вихід. ід. абонента"</string>
<string name="CfMmi" msgid="5123218989141573515">"Переадрес. виклику"</string>
diff --git a/core/res/res/values-vi/strings.xml b/core/res/res/values-vi/strings.xml
index b94f104..329917e 100644
--- a/core/res/res/values-vi/strings.xml
+++ b/core/res/res/values-vi/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Nhập PUK có từ 8 số trở lên."</string>
<string name="needPuk" msgid="919668385956251611">"Thẻ SIM của bạn đã bị khóa PUK. Nhập mã PUK để mở khóa thẻ SIM đó."</string>
<string name="needPuk2" msgid="4526033371987193070">"Nhập mã PUK2 để bỏ chặn thẻ SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"Số gọi đến"</string>
<string name="ClirMmi" msgid="7784673673446833091">"Số gọi đi"</string>
<string name="CfMmi" msgid="5123218989141573515">"Chuyển tiếp cuộc gọi"</string>
diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml
index d7a1e43..d6ed9a9 100644
--- a/core/res/res/values-zh-rCN/strings.xml
+++ b/core/res/res/values-zh-rCN/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"请键入至少 8 位数字的 PUK 码。"</string>
<string name="needPuk" msgid="919668385956251611">"已对 SIM 卡进行 PUK 码锁定。键入 PUK 码将其解锁。"</string>
<string name="needPuk2" msgid="4526033371987193070">"输入 PUK2 码以解锁 SIM 卡。"</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"来电显示"</string>
<string name="ClirMmi" msgid="7784673673446833091">"本机号码"</string>
<string name="CfMmi" msgid="5123218989141573515">"来电转接"</string>
diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml
index 447af8a..bc9337b 100644
--- a/core/res/res/values-zh-rTW/strings.xml
+++ b/core/res/res/values-zh-rTW/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"輸入 8 位數以上的 PUK。"</string>
<string name="needPuk" msgid="919668385956251611">"SIM 卡的 PUK 已鎖定。請輸入 PUK 碼解除鎖定。"</string>
<string name="needPuk2" msgid="4526033371987193070">"請輸入 PUK2 以解鎖 SIM 卡。"</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"來電顯示"</string>
<string name="ClirMmi" msgid="7784673673446833091">"本機號碼"</string>
<string name="CfMmi" msgid="5123218989141573515">"來電轉接"</string>
diff --git a/core/res/res/values-zu/strings.xml b/core/res/res/values-zu/strings.xml
index f43835f..6471965 100644
--- a/core/res/res/values-zu/strings.xml
+++ b/core/res/res/values-zu/strings.xml
@@ -50,6 +50,10 @@
<string name="invalidPuk" msgid="8761456210898036513">"Thayipha i-PUK enezinombolo ezingu-8 noma ngaphezu."</string>
<string name="needPuk" msgid="919668385956251611">"Ikhadi lakho le-SIM livalwe nge-PUK. Thayipha ikhodi ye-PUK ukulivula."</string>
<string name="needPuk2" msgid="4526033371987193070">"Thayipha i-PUK2 ukuze uvule ikhadi le-SIM."</string>
+ <!-- no translation found for imei (2625429890869005782) -->
+ <skip />
+ <!-- no translation found for meid (4841221237681254195) -->
+ <skip />
<string name="ClipMmi" msgid="6952821216480289285">"I-ID Yocingo Olungenayo"</string>
<string name="ClirMmi" msgid="7784673673446833091">"I-ID Yomshayeli Ephumayo"</string>
<string name="CfMmi" msgid="5123218989141573515">"Ukudlulisa ikholi"</string>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index e1bc33b..7b4f50b 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -198,6 +198,8 @@
<java-symbol type="id" name="action0" />
<java-symbol type="id" name="action1" />
<java-symbol type="id" name="action2" />
+ <java-symbol type="id" name="big_picture" />
+ <java-symbol type="id" name="big_text" />
<java-symbol type="attr" name="actionModeShareDrawable" />
<java-symbol type="attr" name="alertDialogCenterButtons" />
@@ -848,6 +850,8 @@
<java-symbol type="string" name="wifi_watchdog_network_disabled" />
<java-symbol type="string" name="wifi_watchdog_network_disabled_detailed" />
<java-symbol type="string" name="yesterday" />
+ <java-symbol type="string" name="imei" />
+ <java-symbol type="string" name="meid" />
<java-symbol type="plurals" name="abbrev_in_num_days" />
<java-symbol type="plurals" name="abbrev_in_num_hours" />
@@ -1070,6 +1074,7 @@
<java-symbol type="layout" name="zoom_controls" />
<java-symbol type="layout" name="zoom_magnify" />
<java-symbol type="layout" name="notification_intruder_content" />
+ <java-symbol type="layout" name="notification_template_big_picture" />
<java-symbol type="anim" name="slide_in_child_bottom" />
<java-symbol type="anim" name="slide_in_right" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 7799f74..2848999 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -97,6 +97,12 @@
the SIM card. -->
<string name="needPuk">Your SIM card is PUK-locked. Type the PUK code to unlock it.</string>
<string name="needPuk2">Type PUK2 to unblock SIM card.</string>
+ <!-- Title for the dialog used to display the user's IMEI number [CHAR LIMIT=10] -->
+ <string name="imei">IMEI</string>
+
+ <!-- Title for the dialog used to display the user's MEID number on CDMA network
+ [CHAR LIMIT=10] -->
+ <string name="meid">MEID</string>
<!-- Displayed as the title for a success/failure report enabling/disabling caller ID. -->
<string name="ClipMmi">Incoming Caller ID</string>
@@ -3186,42 +3192,42 @@
<string name="add_account_button_label">Add account</string>
<!-- NumberPicker - accessibility support -->
- <!-- Description of the button to increment the NumberPicker value. [CHAR LIMIT=NONE] -->
- <string name="number_picker_increment_button">Increment</string>
- <!-- Description of the button to decrement the NumberPicker value. [CHAR LIMIT=NONE] -->
- <string name="number_picker_decrement_button">Decrement</string>
+ <!-- Description of the button to increase the NumberPicker value. [CHAR LIMIT=NONE] -->
+ <string name="number_picker_increment_button">Increase</string>
+ <!-- Description of the button to decrease the NumberPicker value. [CHAR LIMIT=NONE] -->
+ <string name="number_picker_decrement_button">Decrease</string>
<!-- Description of the tap and hold action to get into scroll mode in NumberPicker. [CHAR LIMIT=NONE] -->
<string name="number_picker_increment_scroll_mode"><xliff:g id="value" example="3">%s</xliff:g> touch and hold.</string>
<!-- Description of the scrolling action in NumberPicker. [CHAR LIMIT=NONE] -->
- <string name="number_picker_increment_scroll_action">Slide up to increment and down to decrement.</string>
+ <string name="number_picker_increment_scroll_action">Slide up to increase and down to decrease.</string>
<!-- TimePicker - accessibility support -->
- <!-- Description of the button to increment the TimePicker's minute value. [CHAR LIMIT=NONE] -->
- <string name="time_picker_increment_minute_button">Increment minute</string>
- <!-- Description of the button to decrement the TimePicker's minute value. [CHAR LIMIT=NONE] -->
- <string name="time_picker_decrement_minute_button">Decrement minute</string>
- <!-- Description of the button to increment the TimePicker's hour value. [CHAR LIMIT=NONE] -->
- <string name="time_picker_increment_hour_button">Increment hour</string>
- <!-- Description of the button to decrement the TimePicker's hour value. [CHAR LIMIT=NONE] -->
- <string name="time_picker_decrement_hour_button">Decrement hour</string>
- <!-- Description of the button to increment the TimePicker's set PM value. [CHAR LIMIT=NONE] -->
+ <!-- Description of the button to increase the TimePicker's minute value. [CHAR LIMIT=NONE] -->
+ <string name="time_picker_increment_minute_button">Increase minute</string>
+ <!-- Description of the button to decrease the TimePicker's minute value. [CHAR LIMIT=NONE] -->
+ <string name="time_picker_decrement_minute_button">Decrease minute</string>
+ <!-- Description of the button to increase the TimePicker's hour value. [CHAR LIMIT=NONE] -->
+ <string name="time_picker_increment_hour_button">Increase hour</string>
+ <!-- Description of the button to decrease the TimePicker's hour value. [CHAR LIMIT=NONE] -->
+ <string name="time_picker_decrement_hour_button">Decrease hour</string>
+ <!-- Description of the button to increase the TimePicker's set PM value. [CHAR LIMIT=NONE] -->
<string name="time_picker_increment_set_pm_button">Set PM</string>
- <!-- Description of the button to decrement the TimePicker's set AM value. [CHAR LIMIT=NONE] -->
+ <!-- Description of the button to decrease the TimePicker's set AM value. [CHAR LIMIT=NONE] -->
<string name="time_picker_decrement_set_am_button">Set AM</string>
<!-- DatePicker - accessibility support -->
- <!-- Description of the button to increment the DatePicker's month value. [CHAR LIMIT=NONE] -->
- <string name="date_picker_increment_month_button">Increment month</string>
- <!-- Description of the button to decrement the DatePicker's month value. [CHAR LIMIT=NONE] -->
- <string name="date_picker_decrement_month_button">Decrement month</string>
- <!-- Description of the button to increment the DatePicker's day value. [CHAR LIMIT=NONE] -->
- <string name="date_picker_increment_day_button">Increment day</string>
- <!-- Description of the button to decrement the DatePicker's day value. [CHAR LIMIT=NONE] -->
- <string name="date_picker_decrement_day_button">Decrement day</string>
- <!-- Description of the button to increment the DatePicker's year value. [CHAR LIMIT=NONE] -->
- <string name="date_picker_increment_year_button">Increment year</string>
- <!-- Description of the button to decrement the DatePicker's year value. [CHAR LIMIT=NONE] -->
- <string name="date_picker_decrement_year_button">Decrement year</string>
+ <!-- Description of the button to increase the DatePicker's month value. [CHAR LIMIT=NONE] -->
+ <string name="date_picker_increment_month_button">Increase month</string>
+ <!-- Description of the button to decrease the DatePicker's month value. [CHAR LIMIT=NONE] -->
+ <string name="date_picker_decrement_month_button">Decrease month</string>
+ <!-- Description of the button to increase the DatePicker's day value. [CHAR LIMIT=NONE] -->
+ <string name="date_picker_increment_day_button">Increase day</string>
+ <!-- Description of the button to decrease the DatePicker's day value. [CHAR LIMIT=NONE] -->
+ <string name="date_picker_decrement_day_button">Decrease day</string>
+ <!-- Description of the button to increase the DatePicker's year value. [CHAR LIMIT=NONE] -->
+ <string name="date_picker_increment_year_button">Increase year</string>
+ <!-- Description of the button to decrease the DatePicker's year value. [CHAR LIMIT=NONE] -->
+ <string name="date_picker_decrement_year_button">Decrease year</string>
<!-- CheckBox - accessibility support -->
<!-- Description of the checked state of a CheckBox. [CHAR LIMIT=NONE] -->
diff --git a/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
index 6a471ad..b2075ae 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
@@ -16,12 +16,8 @@
package android.app;
-import coretestutils.http.MockResponse;
-import coretestutils.http.MockWebServer;
-
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
-import android.app.DownloadManagerBaseTest.DataType;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -39,10 +35,14 @@
import android.test.InstrumentationTestCase;
import android.util.Log;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
@@ -53,13 +53,15 @@
import java.util.Set;
import java.util.concurrent.TimeoutException;
+import libcore.io.Streams;
+
/**
* Base class for Instrumented tests for the Download Manager.
*/
public class DownloadManagerBaseTest extends InstrumentationTestCase {
private static final String TAG = "DownloadManagerBaseTest";
protected DownloadManager mDownloadManager = null;
- protected MockWebServer mServer = null;
+ private MockWebServer mServer = null;
protected String mFileType = "text/plain";
protected Context mContext = null;
protected MultipleDownloadsCompletedReceiver mReceiver = null;
@@ -237,63 +239,57 @@
mContext = getInstrumentation().getContext();
mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE);
mServer = new MockWebServer();
+ mServer.play();
mReceiver = registerNewMultipleDownloadsReceiver();
// Note: callers overriding this should call mServer.play() with the desired port #
}
/**
- * Helper to enqueue a response from the MockWebServer with no body.
+ * Helper to build a response from the MockWebServer with no body.
*
* @param status The HTTP status code to return for this response
* @return Returns the mock web server response that was queued (which can be modified)
*/
- protected MockResponse enqueueResponse(int status) {
- return doEnqueueResponse(status);
-
+ protected MockResponse buildResponse(int status) {
+ MockResponse response = new MockResponse().setResponseCode(status);
+ response.setHeader("Content-type", mFileType);
+ return response;
}
/**
- * Helper to enqueue a response from the MockWebServer.
+ * Helper to build a response from the MockWebServer.
*
* @param status The HTTP status code to return for this response
* @param body The body to return in this response
* @return Returns the mock web server response that was queued (which can be modified)
*/
- protected MockResponse enqueueResponse(int status, byte[] body) {
- return doEnqueueResponse(status).setBody(body);
-
+ protected MockResponse buildResponse(int status, byte[] body) {
+ return buildResponse(status).setBody(body);
}
/**
- * Helper to enqueue a response from the MockWebServer.
+ * Helper to build a response from the MockWebServer.
*
* @param status The HTTP status code to return for this response
* @param bodyFile The body to return in this response
* @return Returns the mock web server response that was queued (which can be modified)
*/
- protected MockResponse enqueueResponse(int status, File bodyFile) {
- return doEnqueueResponse(status).setBody(bodyFile);
+ protected MockResponse buildResponse(int status, File bodyFile)
+ throws FileNotFoundException, IOException {
+ final byte[] body = Streams.readFully(new FileInputStream(bodyFile));
+ return buildResponse(status).setBody(body);
}
- /**
- * Helper for enqueue'ing a response from the MockWebServer.
- *
- * @param status The HTTP status code to return for this response
- * @return Returns the mock web server response that was queued (which can be modified)
- */
- protected MockResponse doEnqueueResponse(int status) {
- MockResponse response = new MockResponse().setResponseCode(status);
- response.addHeader("Content-type", mFileType);
- mServer.enqueue(response);
- return response;
+ protected void enqueueResponse(MockResponse resp) {
+ mServer.enqueue(resp);
}
/**
* Helper to generate a random blob of bytes.
*
* @param size The size of the data to generate
- * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
- * {@link DataType.BINARY}.
+ * @param type The type of data to generate: currently, one of {@link DataType#TEXT} or
+ * {@link DataType#BINARY}.
* @return The random data that is generated.
*/
protected byte[] generateData(int size, DataType type) {
@@ -304,8 +300,8 @@
* Helper to generate a random blob of bytes using a given RNG.
*
* @param size The size of the data to generate
- * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
- * {@link DataType.BINARY}.
+ * @param type The type of data to generate: currently, one of {@link DataType#TEXT} or
+ * {@link DataType#BINARY}.
* @param rng (optional) The RNG to use; pass null to use
* @return The random data that is generated.
*/
@@ -492,8 +488,6 @@
assertEquals(1, cursor.getCount());
assertTrue(cursor.moveToFirst());
- mServer.checkForExceptions();
-
verifyFileSize(pfd, fileSize);
verifyFileContents(pfd, fileData);
} finally {
@@ -928,7 +922,7 @@
protected long enqueueDownloadRequest(byte[] body, int location) throws Exception {
// Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, body);
+ mServer.enqueue(buildResponse(HTTP_OK, body));
return doEnqueue(location);
}
@@ -943,7 +937,7 @@
protected long enqueueDownloadRequest(File body, int location) throws Exception {
// Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, body);
+ mServer.enqueue(buildResponse(HTTP_OK, body));
return doEnqueue(location);
}
@@ -1035,4 +1029,4 @@
assertEquals(1, mReceiver.numDownloadsCompleted());
return dlRequest;
}
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java b/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java
index afe7f55..aa9f69d 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java
@@ -16,8 +16,6 @@
package android.app;
-import coretestutils.http.MockResponse;
-
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
import android.database.Cursor;
@@ -26,6 +24,8 @@
import android.os.ParcelFileDescriptor;
import android.test.suitebuilder.annotation.LargeTest;
+import com.google.mockwebserver.MockResponse;
+
import java.io.File;
import java.util.Iterator;
import java.util.Set;
@@ -47,7 +47,6 @@
public void setUp() throws Exception {
super.setUp();
setWiFiStateOn(true);
- mServer.play();
removeAllCurrentDownloads();
}
@@ -132,8 +131,6 @@
assertEquals(1, cursor.getCount());
assertTrue(cursor.moveToFirst());
- mServer.checkForExceptions();
-
verifyFileSize(pfd, fileSize);
verifyFileContents(pfd, fileData);
int colIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
@@ -154,7 +151,7 @@
byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
// Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, blobData);
+ enqueueResponse(buildResponse(HTTP_OK, blobData));
try {
Uri uri = getServerUri(DEFAULT_FILENAME);
@@ -193,7 +190,7 @@
byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
// Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, blobData);
+ enqueueResponse(buildResponse(HTTP_OK, blobData));
Uri uri = getServerUri(DEFAULT_FILENAME);
Request request = new Request(uri);
@@ -224,7 +221,7 @@
byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
// Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, blobData);
+ enqueueResponse(buildResponse(HTTP_OK, blobData));
Uri uri = getServerUri(DEFAULT_FILENAME);
Request request = new Request(uri);
@@ -251,7 +248,7 @@
public void testGetDownloadIdOnNotification() throws Exception {
byte[] blobData = generateData(3000, DataType.TEXT); // file size = 3000 bytes
- MockResponse response = enqueueResponse(HTTP_OK, blobData);
+ enqueueResponse(buildResponse(HTTP_OK, blobData));
long dlRequest = doCommonStandardEnqueue();
waitForDownloadOrTimeout(dlRequest);
@@ -271,8 +268,9 @@
// force 6 redirects
for (int i = 0; i < 6; ++i) {
- MockResponse response = enqueueResponse(HTTP_REDIRECT);
- response.addHeader("Location", uri.toString());
+ final MockResponse resp = buildResponse(HTTP_REDIRECT);
+ resp.setHeader("Location", uri.toString());
+ enqueueResponse(resp);
}
doErrorTest(uri, DownloadManager.ERROR_TOO_MANY_REDIRECTS);
}
@@ -283,7 +281,7 @@
@LargeTest
public void testErrorUnhandledHttpCode() throws Exception {
Uri uri = getServerUri(DEFAULT_FILENAME);
- MockResponse response = enqueueResponse(HTTP_PARTIAL_CONTENT);
+ enqueueResponse(buildResponse(HTTP_PARTIAL_CONTENT));
doErrorTest(uri, DownloadManager.ERROR_UNHANDLED_HTTP_CODE);
}
@@ -294,8 +292,9 @@
@LargeTest
public void testErrorHttpDataError_invalidRedirect() throws Exception {
Uri uri = getServerUri(DEFAULT_FILENAME);
- MockResponse response = enqueueResponse(HTTP_REDIRECT);
- response.addHeader("Location", "://blah.blah.blah.com");
+ final MockResponse resp = buildResponse(HTTP_REDIRECT);
+ resp.setHeader("Location", "://blah.blah.blah.com");
+ enqueueResponse(resp);
doErrorTest(uri, DownloadManager.ERROR_HTTP_DATA_ERROR);
}
@@ -327,7 +326,7 @@
public void testSetTitle() throws Exception {
int fileSize = 1024;
byte[] blobData = generateData(fileSize, DataType.BINARY);
- MockResponse response = enqueueResponse(HTTP_OK, blobData);
+ enqueueResponse(buildResponse(HTTP_OK, blobData));
// An arbitrary unicode string title
final String title = "\u00a5123;\"\u0152\u017d \u054b \u0a07 \ucce0 \u6820\u03a8\u5c34" +
@@ -359,7 +358,7 @@
byte[] blobData = generateData(fileSize, DataType.TEXT);
setWiFiStateOn(false);
- enqueueResponse(HTTP_OK, blobData);
+ enqueueResponse(buildResponse(HTTP_OK, blobData));
try {
Uri uri = getServerUri(DEFAULT_FILENAME);
@@ -383,32 +382,16 @@
}
/**
- * Tests when the server drops the connection after all headers (but before any data send).
- */
- @LargeTest
- public void testDropConnection_headers() throws Exception {
- byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
-
- MockResponse response = enqueueResponse(HTTP_OK, blobData);
- response.setCloseConnectionAfterHeader("content-length");
- long dlRequest = doCommonStandardEnqueue();
-
- // Download will never complete when header is dropped
- boolean success = waitForDownloadOrTimeoutNoThrow(dlRequest, DEFAULT_WAIT_POLL_TIME,
- DEFAULT_MAX_WAIT_TIME);
-
- assertFalse(success);
- }
-
- /**
* Tests that we get an error code when the server drops the connection during a download.
*/
@LargeTest
public void testServerDropConnection_body() throws Exception {
byte[] blobData = generateData(25000, DataType.TEXT); // file size = 25000 bytes
- MockResponse response = enqueueResponse(HTTP_OK, blobData);
- response.setCloseConnectionAfterXBytes(15382);
+ final MockResponse resp = buildResponse(HTTP_OK, blobData);
+ resp.setHeader("Content-Length", "50000");
+ enqueueResponse(resp);
+
long dlRequest = doCommonStandardEnqueue();
waitForDownloadOrTimeout(dlRequest);
diff --git a/core/tests/coretests/src/android/app/DownloadManagerStressTest.java b/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
index bdeb554..864b2d6 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
@@ -46,7 +46,6 @@
public void setUp() throws Exception {
super.setUp();
setWiFiStateOn(true);
- mServer.play();
removeAllCurrentDownloads();
}
@@ -85,7 +84,7 @@
request.setTitle(String.format("%s--%d", DEFAULT_FILENAME + i, i));
// Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, blobData);
+ enqueueResponse(buildResponse(HTTP_OK, blobData));
long requestID = mDownloadManager.enqueue(request);
}
@@ -127,7 +126,7 @@
try {
long dlRequest = doStandardEnqueue(largeFile);
- // wait for the download to complete
+ // wait for the download to complete
waitForDownloadOrTimeout(dlRequest);
ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest);
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
index a419068..09dcac5 100644
--- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
+++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
@@ -20,7 +20,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_STATIC_JAVA_LIBRARIES := android-common frameworks-core-util-lib
+LOCAL_STATIC_JAVA_LIBRARIES := android-common mockwebserver
LOCAL_SDK_VERSION := current
LOCAL_PACKAGE_NAME := DownloadManagerTestApp
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java
similarity index 73%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java
index 334661d..8e935f8 100644
--- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java
+++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java
@@ -18,7 +18,6 @@
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
-import android.app.DownloadManager.Request;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -26,37 +25,19 @@
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.net.Uri;
import android.net.wifi.WifiManager;
-import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
-import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.provider.Settings;
import android.test.InstrumentationTestCase;
import android.util.Log;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.net.URL;
-import java.util.concurrent.TimeoutException;
import java.util.Collections;
import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Random;
import java.util.Set;
-import java.util.Vector;
-
-import junit.framework.AssertionFailedError;
-
-import coretestutils.http.MockResponse;
-import coretestutils.http.MockWebServer;
+import java.util.concurrent.TimeoutException;
/**
* Base class for Instrumented tests for the Download Manager.
@@ -64,7 +45,6 @@
public class DownloadManagerBaseTest extends InstrumentationTestCase {
protected DownloadManager mDownloadManager = null;
- protected MockWebServer mServer = null;
protected String mFileType = "text/plain";
protected Context mContext = null;
protected MultipleDownloadsCompletedReceiver mReceiver = null;
@@ -77,7 +57,6 @@
protected static final int HTTP_PARTIAL_CONTENT = 206;
protected static final int HTTP_NOT_FOUND = 404;
protected static final int HTTP_SERVICE_UNAVAILABLE = 503;
- protected String DEFAULT_FILENAME = "somefile.txt";
protected static final int DEFAULT_MAX_WAIT_TIME = 2 * 60 * 1000; // 2 minutes
protected static final int DEFAULT_WAIT_POLL_TIME = 5 * 1000; // 5 seconds
@@ -86,48 +65,6 @@
protected static final int MAX_WAIT_FOR_DOWNLOAD_TIME = 5 * 60 * 1000; // 5 minutes
protected static final int MAX_WAIT_FOR_LARGE_DOWNLOAD_TIME = 15 * 60 * 1000; // 15 minutes
- protected static final int DOWNLOAD_TO_SYSTEM_CACHE = 1;
- protected static final int DOWNLOAD_TO_DOWNLOAD_CACHE_DIR = 2;
-
- // Just a few popular file types used to return from a download
- protected enum DownloadFileType {
- PLAINTEXT,
- APK,
- GIF,
- GARBAGE,
- UNRECOGNIZED,
- ZIP
- }
-
- protected enum DataType {
- TEXT,
- BINARY
- }
-
- public static class LoggingRng extends Random {
-
- /**
- * Constructor
- *
- * Creates RNG with self-generated seed value.
- */
- public LoggingRng() {
- this(SystemClock.uptimeMillis());
- }
-
- /**
- * Constructor
- *
- * Creats RNG with given initial seed value
-
- * @param seed The initial seed value
- */
- public LoggingRng(long seed) {
- super(seed);
- Log.i(LOG_TAG, "Seeding RNG with value: " + seed);
- }
- }
-
public static class MultipleDownloadsCompletedReceiver extends BroadcastReceiver {
private volatile int mNumDownloadsCompleted = 0;
private Set<Long> downloadIds = Collections.synchronizedSet(new HashSet<Long>());
@@ -171,7 +108,7 @@
/**
* Gets the number of times the {@link #onReceive} callback has been called for the
- * {@link DownloadManager.ACTION_DOWNLOAD_COMPLETED} action, indicating the number of
+ * {@link DownloadManager#ACTION_DOWNLOAD_COMPLETE} action, indicating the number of
* downloads completed thus far.
*
* @return the number of downloads completed so far.
@@ -241,76 +178,7 @@
public void setUp() throws Exception {
mContext = getInstrumentation().getContext();
mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE);
- mServer = new MockWebServer();
mReceiver = registerNewMultipleDownloadsReceiver();
- // Note: callers overriding this should call mServer.play() with the desired port #
- }
-
- /**
- * Helper to enqueue a response from the MockWebServer.
- *
- * @param status The HTTP status code to return for this response
- * @param body The body to return in this response
- * @return Returns the mock web server response that was queued (which can be modified)
- */
- private MockResponse enqueueResponse(int status, byte[] body) {
- return doEnqueueResponse(status).setBody(body);
-
- }
-
- /**
- * Helper to enqueue a response from the MockWebServer.
- *
- * @param status The HTTP status code to return for this response
- * @param bodyFile The body to return in this response
- * @return Returns the mock web server response that was queued (which can be modified)
- */
- private MockResponse enqueueResponse(int status, File bodyFile) {
- return doEnqueueResponse(status).setBody(bodyFile);
- }
-
- /**
- * Helper for enqueue'ing a response from the MockWebServer.
- *
- * @param status The HTTP status code to return for this response
- * @return Returns the mock web server response that was queued (which can be modified)
- */
- private MockResponse doEnqueueResponse(int status) {
- MockResponse response = new MockResponse().setResponseCode(status);
- response.addHeader("Content-type", mFileType);
- mServer.enqueue(response);
- return response;
- }
-
- /**
- * Helper to generate a random blob of bytes using a given RNG.
- *
- * @param size The size of the data to generate
- * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
- * {@link DataType.BINARY}.
- * @param rng (optional) The RNG to use; pass null to use
- * @return The random data that is generated.
- */
- private byte[] generateData(int size, DataType type, Random rng) {
- int min = Byte.MIN_VALUE;
- int max = Byte.MAX_VALUE;
-
- // Only use chars in the HTTP ASCII printable character range for Text
- if (type == DataType.TEXT) {
- min = 32;
- max = 126;
- }
- byte[] result = new byte[size];
- Log.i(LOG_TAG, "Generating data of size: " + size);
-
- if (rng == null) {
- rng = new LoggingRng();
- }
-
- for (int i = 0; i < size; ++i) {
- result[i] = (byte) (min + rng.nextInt(max - min + 1));
- }
- return result;
}
/**
@@ -324,76 +192,6 @@
}
/**
- * Helper to verify the contents of a downloaded file versus a byte[].
- *
- * @param actual The file of whose contents to verify
- * @param expected The data we expect to find in the aforementioned file
- * @throws IOException if there was a problem reading from the file
- */
- private void verifyFileContents(ParcelFileDescriptor actual, byte[] expected)
- throws IOException {
- AutoCloseInputStream input = new ParcelFileDescriptor.AutoCloseInputStream(actual);
- long fileSize = actual.getStatSize();
-
- assertTrue(fileSize <= Integer.MAX_VALUE);
- assertEquals(expected.length, fileSize);
-
- byte[] actualData = new byte[expected.length];
- assertEquals(input.read(actualData), fileSize);
- compareByteArrays(actualData, expected);
- }
-
- /**
- * Helper to compare 2 byte arrays.
- *
- * @param actual The array whose data we want to verify
- * @param expected The array of data we expect to see
- */
- private void compareByteArrays(byte[] actual, byte[] expected) {
- assertEquals(actual.length, expected.length);
- int length = actual.length;
- for (int i = 0; i < length; ++i) {
- // assert has a bit of overhead, so only do the assert when the values are not the same
- if (actual[i] != expected[i]) {
- fail("Byte arrays are not equal.");
- }
- }
- }
-
- /**
- * Gets the MIME content string for a given type
- *
- * @param type The MIME type to return
- * @return the String representation of that MIME content type
- */
- protected String getMimeMapping(DownloadFileType type) {
- switch (type) {
- case APK:
- return "application/vnd.android.package-archive";
- case GIF:
- return "image/gif";
- case ZIP:
- return "application/x-zip-compressed";
- case GARBAGE:
- return "zip\\pidy/doo/da";
- case UNRECOGNIZED:
- return "application/new.undefined.type.of.app";
- }
- return "text/plain";
- }
-
- /**
- * Gets the Uri that should be used to access the mock server
- *
- * @param filename The name of the file to try to retrieve from the mock server
- * @return the Uri to use for access the file on the mock server
- */
- private Uri getServerUri(String filename) throws Exception {
- URL url = mServer.getUrl("/" + filename);
- return Uri.parse(url.toString());
- }
-
- /**
* Helper to create and register a new MultipleDownloadCompletedReciever
*
* This is used to track many simultaneous downloads by keeping count of all the downloads
@@ -738,39 +536,6 @@
}
/**
- * Helper to perform a standard enqueue of data to the mock server.
- * download is performed to the downloads cache dir (NOT systemcache dir)
- *
- * @param body The body to return in the response from the server
- */
- private long doStandardEnqueue(byte[] body) throws Exception {
- // Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, body);
- return doCommonStandardEnqueue();
- }
-
- /**
- * Helper to perform a standard enqueue of data to the mock server.
- *
- * @param body The body to return in the response from the server, contained in the file
- */
- private long doStandardEnqueue(File body) throws Exception {
- // Prepare the mock server with a standard response
- enqueueResponse(HTTP_OK, body);
- return doCommonStandardEnqueue();
- }
-
- /**
- * Helper to do the additional steps (setting title and Uri of default filename) when
- * doing a standard enqueue request to the server.
- */
- private long doCommonStandardEnqueue() throws Exception {
- Uri uri = getServerUri(DEFAULT_FILENAME);
- Request request = new Request(uri).setTitle(DEFAULT_FILENAME);
- return mDownloadManager.enqueue(request);
- }
-
- /**
* Helper to verify an int value in a Cursor
*
* @param cursor The cursor containing the query results
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java
similarity index 97%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java
index 654f747..9c44d61 100644
--- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java
+++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java
@@ -16,16 +16,11 @@
package com.android.frameworks.downloadmanagertests;
import android.app.DownloadManager;
-import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
-import android.content.Context;
-import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
-import android.provider.Settings;
-import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log;
import java.io.DataInputStream;
@@ -33,13 +28,8 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
-import java.io.FileWriter;
import java.util.HashSet;
-import coretestutils.http.MockResponse;
-import coretestutils.http.MockWebServer;
-import coretestutils.http.RecordedRequest;
-
/**
* Class to test downloading files from a real (not mock) external server.
*/
@@ -243,7 +233,7 @@
Uri remoteUri = getExternalFileUri(filename);
Request request = new Request(remoteUri);
- request.setMimeType(getMimeMapping(DownloadFileType.APK));
+ request.setMimeType("application/vnd.android.package-archive");
dlRequest = mDownloadManager.enqueue(request);
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestRunner.java
similarity index 100%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestRunner.java
diff --git a/core/tests/utillib/src/coretestutils/http/MockResponse.java b/core/tests/utillib/src/coretestutils/http/MockResponse.java
deleted file mode 100644
index 5b03e5f..0000000
--- a/core/tests/utillib/src/coretestutils/http/MockResponse.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package coretestutils.http;
-
-import static coretestutils.http.MockWebServer.ASCII;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import android.util.Log;
-
-/**
- * A scripted response to be replayed by the mock web server.
- */
-public class MockResponse {
- private static final byte[] EMPTY_BODY = new byte[0];
- static final String LOG_TAG = "coretestutils.http.MockResponse";
-
- private String status = "HTTP/1.1 200 OK";
- private Map<String, String> headers = new HashMap<String, String>();
- private byte[] body = EMPTY_BODY;
- private boolean closeConnectionAfter = false;
- private String closeConnectionAfterHeader = null;
- private int closeConnectionAfterXBytes = -1;
- private int pauseConnectionAfterXBytes = -1;
- private File bodyExternalFile = null;
-
- public MockResponse() {
- addHeader("Content-Length", 0);
- }
-
- /**
- * Returns the HTTP response line, such as "HTTP/1.1 200 OK".
- */
- public String getStatus() {
- return status;
- }
-
- public MockResponse setResponseCode(int code) {
- this.status = "HTTP/1.1 " + code + " OK";
- return this;
- }
-
- /**
- * Returns the HTTP headers, such as "Content-Length: 0".
- */
- public List<String> getHeaders() {
- List<String> headerStrings = new ArrayList<String>();
- for (String header : headers.keySet()) {
- headerStrings.add(header + ": " + headers.get(header));
- }
- return headerStrings;
- }
-
- public MockResponse addHeader(String header, String value) {
- headers.put(header.toLowerCase(), value);
- return this;
- }
-
- public MockResponse addHeader(String header, long value) {
- return addHeader(header, Long.toString(value));
- }
-
- public MockResponse removeHeader(String header) {
- headers.remove(header.toLowerCase());
- return this;
- }
-
- /**
- * Returns true if the body should come from an external file, false otherwise.
- */
- private boolean bodyIsExternal() {
- return bodyExternalFile != null;
- }
-
- /**
- * Returns an input stream containing the raw HTTP payload.
- */
- public InputStream getBody() {
- if (bodyIsExternal()) {
- try {
- return new FileInputStream(bodyExternalFile);
- } catch (FileNotFoundException e) {
- Log.e(LOG_TAG, "File not found: " + bodyExternalFile.getAbsolutePath());
- }
- }
- return new ByteArrayInputStream(this.body);
- }
-
- public MockResponse setBody(File body) {
- addHeader("Content-Length", body.length());
- this.bodyExternalFile = body;
- return this;
- }
-
- public MockResponse setBody(byte[] body) {
- addHeader("Content-Length", body.length);
- this.body = body;
- return this;
- }
-
- public MockResponse setBody(String body) {
- try {
- return setBody(body.getBytes(ASCII));
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError();
- }
- }
-
- /**
- * Sets the body as chunked.
- *
- * Currently chunked body is not supported for external files as bodies.
- */
- public MockResponse setChunkedBody(byte[] body, int maxChunkSize) throws IOException {
- addHeader("Transfer-encoding", "chunked");
-
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- int pos = 0;
- while (pos < body.length) {
- int chunkSize = Math.min(body.length - pos, maxChunkSize);
- bytesOut.write(Integer.toHexString(chunkSize).getBytes(ASCII));
- bytesOut.write("\r\n".getBytes(ASCII));
- bytesOut.write(body, pos, chunkSize);
- bytesOut.write("\r\n".getBytes(ASCII));
- pos += chunkSize;
- }
- bytesOut.write("0\r\n".getBytes(ASCII));
- this.body = bytesOut.toByteArray();
- return this;
- }
-
- public MockResponse setChunkedBody(String body, int maxChunkSize) throws IOException {
- return setChunkedBody(body.getBytes(ASCII), maxChunkSize);
- }
-
- @Override public String toString() {
- return status;
- }
-
- public boolean shouldCloseConnectionAfter() {
- return closeConnectionAfter;
- }
-
- public MockResponse setCloseConnectionAfter(boolean closeConnectionAfter) {
- this.closeConnectionAfter = closeConnectionAfter;
- return this;
- }
-
- /**
- * Sets the header after which sending the server should close the connection.
- */
- public MockResponse setCloseConnectionAfterHeader(String header) {
- closeConnectionAfterHeader = header;
- setCloseConnectionAfter(true);
- return this;
- }
-
- /**
- * Returns the header after which sending the server should close the connection.
- */
- public String getCloseConnectionAfterHeader() {
- return closeConnectionAfterHeader;
- }
-
- /**
- * Sets the number of bytes in the body to send before which the server should close the
- * connection. Set to -1 to unset and send the entire body (default).
- */
- public MockResponse setCloseConnectionAfterXBytes(int position) {
- closeConnectionAfterXBytes = position;
- setCloseConnectionAfter(true);
- return this;
- }
-
- /**
- * Returns the number of bytes in the body to send before which the server should close the
- * connection. Returns -1 if the entire body should be sent (default).
- */
- public int getCloseConnectionAfterXBytes() {
- return closeConnectionAfterXBytes;
- }
-
- /**
- * Sets the number of bytes in the body to send before which the server should pause the
- * connection (stalls in sending data). Only one pause per response is supported.
- * Set to -1 to unset pausing (default).
- */
- public MockResponse setPauseConnectionAfterXBytes(int position) {
- pauseConnectionAfterXBytes = position;
- return this;
- }
-
- /**
- * Returns the number of bytes in the body to send before which the server should pause the
- * connection (stalls in sending data). (Returns -1 if it should not pause).
- */
- public int getPauseConnectionAfterXBytes() {
- return pauseConnectionAfterXBytes;
- }
-
- /**
- * Returns true if this response is flagged to pause the connection mid-stream, false otherwise
- */
- public boolean getShouldPause() {
- return (pauseConnectionAfterXBytes != -1);
- }
-
- /**
- * Returns true if this response is flagged to close the connection mid-stream, false otherwise
- */
- public boolean getShouldClose() {
- return (closeConnectionAfterXBytes != -1);
- }
-}
diff --git a/core/tests/utillib/src/coretestutils/http/MockWebServer.java b/core/tests/utillib/src/coretestutils/http/MockWebServer.java
deleted file mode 100644
index c329ffa..0000000
--- a/core/tests/utillib/src/coretestutils/http/MockWebServer.java
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package coretestutils.http;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.MalformedURLException;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import android.util.Log;
-
-/**
- * A scriptable web server. Callers supply canned responses and the server
- * replays them upon request in sequence.
- *
- * TODO: merge with the version from libcore/support/src/tests/java once it's in.
- */
-public final class MockWebServer {
- static final String ASCII = "US-ASCII";
- static final String LOG_TAG = "coretestutils.http.MockWebServer";
-
- private final BlockingQueue<RecordedRequest> requestQueue
- = new LinkedBlockingQueue<RecordedRequest>();
- private final BlockingQueue<MockResponse> responseQueue
- = new LinkedBlockingQueue<MockResponse>();
- private int bodyLimit = Integer.MAX_VALUE;
- private final ExecutorService executor = Executors.newCachedThreadPool();
- // keep Futures around so we can rethrow any exceptions thrown by Callables
- private final Queue<Future<?>> futures = new LinkedList<Future<?>>();
- private final Object downloadPauseLock = new Object();
- // global flag to signal when downloads should resume on the server
- private volatile boolean downloadResume = false;
-
- private int port = -1;
-
- public int getPort() {
- if (port == -1) {
- throw new IllegalStateException("Cannot retrieve port before calling play()");
- }
- return port;
- }
-
- /**
- * Returns a URL for connecting to this server.
- *
- * @param path the request path, such as "/".
- */
- public URL getUrl(String path) throws MalformedURLException {
- return new URL("http://localhost:" + getPort() + path);
- }
-
- /**
- * Sets the number of bytes of the POST body to keep in memory to the given
- * limit.
- */
- public void setBodyLimit(int maxBodyLength) {
- this.bodyLimit = maxBodyLength;
- }
-
- public void enqueue(MockResponse response) {
- responseQueue.add(response);
- }
-
- /**
- * Awaits the next HTTP request, removes it, and returns it. Callers should
- * use this to verify the request sent was as intended.
- */
- public RecordedRequest takeRequest() throws InterruptedException {
- return requestQueue.take();
- }
-
- public RecordedRequest takeRequestWithTimeout(long timeoutMillis) throws InterruptedException {
- return requestQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
- }
-
- public List<RecordedRequest> drainRequests() {
- List<RecordedRequest> requests = new ArrayList<RecordedRequest>();
- requestQueue.drainTo(requests);
- return requests;
- }
-
- /**
- * Starts the server, serves all enqueued requests, and shuts the server
- * down using the default (server-assigned) port.
- */
- public void play() throws IOException {
- play(0);
- }
-
- /**
- * Starts the server, serves all enqueued requests, and shuts the server
- * down.
- *
- * @param port The port number to use to listen to connections on; pass in 0 to have the
- * server automatically assign a free port
- */
- public void play(int portNumber) throws IOException {
- final ServerSocket ss = new ServerSocket(portNumber);
- ss.setReuseAddress(true);
- port = ss.getLocalPort();
- submitCallable(new Callable<Void>() {
- public Void call() throws Exception {
- int count = 0;
- while (true) {
- if (count > 0 && responseQueue.isEmpty()) {
- ss.close();
- executor.shutdown();
- return null;
- }
-
- serveConnection(ss.accept());
- count++;
- }
- }
- });
- }
-
- private void serveConnection(final Socket s) {
- submitCallable(new Callable<Void>() {
- public Void call() throws Exception {
- InputStream in = new BufferedInputStream(s.getInputStream());
- OutputStream out = new BufferedOutputStream(s.getOutputStream());
-
- int sequenceNumber = 0;
- while (true) {
- RecordedRequest request = readRequest(in, sequenceNumber);
- if (request == null) {
- if (sequenceNumber == 0) {
- throw new IllegalStateException("Connection without any request!");
- } else {
- break;
- }
- }
- requestQueue.add(request);
- MockResponse response = computeResponse(request);
- writeResponse(out, response);
- if (response.shouldCloseConnectionAfter()) {
- break;
- }
- sequenceNumber++;
- }
-
- in.close();
- out.close();
- return null;
- }
- });
- }
-
- private void submitCallable(Callable<?> callable) {
- Future<?> future = executor.submit(callable);
- futures.add(future);
- }
-
- /**
- * Check for and raise any exceptions that have been thrown by child threads. Will not block on
- * children still running.
- * @throws ExecutionException for the first child thread that threw an exception
- */
- public void checkForExceptions() throws ExecutionException, InterruptedException {
- final int originalSize = futures.size();
- for (int i = 0; i < originalSize; i++) {
- Future<?> future = futures.remove();
- try {
- future.get(0, TimeUnit.SECONDS);
- } catch (TimeoutException e) {
- futures.add(future); // still running
- }
- }
- }
-
- /**
- * @param sequenceNumber the index of this request on this connection.
- */
- private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException {
- String request = readAsciiUntilCrlf(in);
- if (request.equals("")) {
- return null; // end of data; no more requests
- }
-
- List<String> headers = new ArrayList<String>();
- int contentLength = -1;
- boolean chunked = false;
- String header;
- while (!(header = readAsciiUntilCrlf(in)).equals("")) {
- headers.add(header);
- String lowercaseHeader = header.toLowerCase();
- if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) {
- contentLength = Integer.parseInt(header.substring(15).trim());
- }
- if (lowercaseHeader.startsWith("transfer-encoding:") &&
- lowercaseHeader.substring(18).trim().equals("chunked")) {
- chunked = true;
- }
- }
-
- boolean hasBody = false;
- TruncatingOutputStream requestBody = new TruncatingOutputStream();
- List<Integer> chunkSizes = new ArrayList<Integer>();
- if (contentLength != -1) {
- hasBody = true;
- transfer(contentLength, in, requestBody);
- } else if (chunked) {
- hasBody = true;
- while (true) {
- int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16);
- if (chunkSize == 0) {
- readEmptyLine(in);
- break;
- }
- chunkSizes.add(chunkSize);
- transfer(chunkSize, in, requestBody);
- readEmptyLine(in);
- }
- }
-
- if (request.startsWith("GET ")) {
- if (hasBody) {
- throw new IllegalArgumentException("GET requests should not have a body!");
- }
- } else if (request.startsWith("POST ")) {
- if (!hasBody) {
- throw new IllegalArgumentException("POST requests must have a body!");
- }
- } else {
- throw new UnsupportedOperationException("Unexpected method: " + request);
- }
-
- return new RecordedRequest(request, headers, chunkSizes,
- requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber);
- }
-
- /**
- * Returns a response to satisfy {@code request}.
- */
- private MockResponse computeResponse(RecordedRequest request) throws InterruptedException {
- if (responseQueue.isEmpty()) {
- throw new IllegalStateException("Unexpected request: " + request);
- }
- return responseQueue.take();
- }
-
- private void writeResponse(OutputStream out, MockResponse response) throws IOException {
- out.write((response.getStatus() + "\r\n").getBytes(ASCII));
- boolean doCloseConnectionAfterHeader = (response.getCloseConnectionAfterHeader() != null);
-
- // Send headers
- String closeConnectionAfterHeader = response.getCloseConnectionAfterHeader();
- for (String header : response.getHeaders()) {
- out.write((header + "\r\n").getBytes(ASCII));
-
- if (doCloseConnectionAfterHeader && header.startsWith(closeConnectionAfterHeader)) {
- Log.i(LOG_TAG, "Closing connection after header" + header);
- break;
- }
- }
-
- // Send actual body data
- if (!doCloseConnectionAfterHeader) {
- out.write(("\r\n").getBytes(ASCII));
-
- InputStream body = response.getBody();
- final int READ_BLOCK_SIZE = 10000; // process blocks this size
- byte[] currentBlock = new byte[READ_BLOCK_SIZE];
- int currentBlockSize = 0;
- int writtenSoFar = 0;
-
- boolean shouldPause = response.getShouldPause();
- boolean shouldClose = response.getShouldClose();
- int pause = response.getPauseConnectionAfterXBytes();
- int close = response.getCloseConnectionAfterXBytes();
-
- // Don't bother pausing if it's set to pause -after- the connection should be dropped
- if (shouldPause && shouldClose && (pause > close)) {
- shouldPause = false;
- }
-
- // Process each block we read in...
- while ((currentBlockSize = body.read(currentBlock)) != -1) {
- int startIndex = 0;
- int writeLength = currentBlockSize;
-
- // handle the case of pausing
- if (shouldPause && (writtenSoFar + currentBlockSize >= pause)) {
- writeLength = pause - writtenSoFar;
- out.write(currentBlock, 0, writeLength);
- out.flush();
- writtenSoFar += writeLength;
-
- // now pause...
- try {
- Log.i(LOG_TAG, "Pausing connection after " + pause + " bytes");
- // Wait until someone tells us to resume sending...
- synchronized(downloadPauseLock) {
- while (!downloadResume) {
- downloadPauseLock.wait();
- }
- // reset resume back to false
- downloadResume = false;
- }
- } catch (InterruptedException e) {
- Log.e(LOG_TAG, "Server was interrupted during pause in download.");
- }
-
- startIndex = writeLength;
- writeLength = currentBlockSize - writeLength;
- }
-
- // handle the case of closing the connection
- if (shouldClose && (writtenSoFar + writeLength > close)) {
- writeLength = close - writtenSoFar;
- out.write(currentBlock, startIndex, writeLength);
- writtenSoFar += writeLength;
- Log.i(LOG_TAG, "Closing connection after " + close + " bytes");
- break;
- }
- out.write(currentBlock, startIndex, writeLength);
- writtenSoFar += writeLength;
- }
- }
- out.flush();
- }
-
- /**
- * Transfer bytes from {@code in} to {@code out} until either {@code length}
- * bytes have been transferred or {@code in} is exhausted.
- */
- private void transfer(int length, InputStream in, OutputStream out) throws IOException {
- byte[] buffer = new byte[1024];
- while (length > 0) {
- int count = in.read(buffer, 0, Math.min(buffer.length, length));
- if (count == -1) {
- return;
- }
- out.write(buffer, 0, count);
- length -= count;
- }
- }
-
- /**
- * Returns the text from {@code in} until the next "\r\n", or null if
- * {@code in} is exhausted.
- */
- private String readAsciiUntilCrlf(InputStream in) throws IOException {
- StringBuilder builder = new StringBuilder();
- while (true) {
- int c = in.read();
- if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') {
- builder.deleteCharAt(builder.length() - 1);
- return builder.toString();
- } else if (c == -1) {
- return builder.toString();
- } else {
- builder.append((char) c);
- }
- }
- }
-
- private void readEmptyLine(InputStream in) throws IOException {
- String line = readAsciiUntilCrlf(in);
- if (!line.equals("")) {
- throw new IllegalStateException("Expected empty but was: " + line);
- }
- }
-
- /**
- * An output stream that drops data after bodyLimit bytes.
- */
- private class TruncatingOutputStream extends ByteArrayOutputStream {
- private int numBytesReceived = 0;
- @Override public void write(byte[] buffer, int offset, int len) {
- numBytesReceived += len;
- super.write(buffer, offset, Math.min(len, bodyLimit - count));
- }
- @Override public void write(int oneByte) {
- numBytesReceived++;
- if (count < bodyLimit) {
- super.write(oneByte);
- }
- }
- }
-
- /**
- * Trigger the server to resume sending the download
- */
- public void doResumeDownload() {
- synchronized (downloadPauseLock) {
- downloadResume = true;
- downloadPauseLock.notifyAll();
- }
- }
-}
diff --git a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java b/core/tests/utillib/src/coretestutils/http/RecordedRequest.java
deleted file mode 100644
index 293ff80..0000000
--- a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package coretestutils.http;
-
-import java.util.List;
-
-/**
- * An HTTP request that came into the mock web server.
- */
-public final class RecordedRequest {
- private final String requestLine;
- private final List<String> headers;
- private final List<Integer> chunkSizes;
- private final int bodySize;
- private final byte[] body;
- private final int sequenceNumber;
-
- RecordedRequest(String requestLine, List<String> headers, List<Integer> chunkSizes,
- int bodySize, byte[] body, int sequenceNumber) {
- this.requestLine = requestLine;
- this.headers = headers;
- this.chunkSizes = chunkSizes;
- this.bodySize = bodySize;
- this.body = body;
- this.sequenceNumber = sequenceNumber;
- }
-
- public String getRequestLine() {
- return requestLine;
- }
-
- public List<String> getHeaders() {
- return headers;
- }
-
- /**
- * Returns the sizes of the chunks of this request's body, or an empty list
- * if the request's body was empty or unchunked.
- */
- public List<Integer> getChunkSizes() {
- return chunkSizes;
- }
-
- /**
- * Returns the total size of the body of this POST request (before
- * truncation).
- */
- public int getBodySize() {
- return bodySize;
- }
-
- /**
- * Returns the body of this POST request. This may be truncated.
- */
- public byte[] getBody() {
- return body;
- }
-
- /**
- * Returns the index of this request on its HTTP connection. Since a single
- * HTTP connection may serve multiple requests, each request is assigned its
- * own sequence number.
- */
- public int getSequenceNumber() {
- return sequenceNumber;
- }
-
- @Override public String toString() {
- return requestLine;
- }
-
- public String getMethod() {
- return getRequestLine().split(" ")[0];
- }
-
- public String getPath() {
- return getRequestLine().split(" ")[1];
- }
-}
diff --git a/docs/html/resources/articles/images/spellcheck_client_flow.png b/docs/html/resources/articles/images/spellcheck_client_flow.png
new file mode 100644
index 0000000..4e097aa
--- /dev/null
+++ b/docs/html/resources/articles/images/spellcheck_client_flow.png
Binary files differ
diff --git a/docs/html/resources/articles/images/spellcheck_lifecycle.png b/docs/html/resources/articles/images/spellcheck_lifecycle.png
new file mode 100644
index 0000000..0b10824
--- /dev/null
+++ b/docs/html/resources/articles/images/spellcheck_lifecycle.png
Binary files differ
diff --git a/docs/html/resources/articles/images/textview_spellcheck_screenshot_1.png b/docs/html/resources/articles/images/textview_spellcheck_screenshot_1.png
new file mode 100644
index 0000000..deb47c4
--- /dev/null
+++ b/docs/html/resources/articles/images/textview_spellcheck_screenshot_1.png
Binary files differ
diff --git a/docs/html/resources/articles/images/textview_spellcheck_screenshot_2.png b/docs/html/resources/articles/images/textview_spellcheck_screenshot_2.png
new file mode 100644
index 0000000..e3af4c5
--- /dev/null
+++ b/docs/html/resources/articles/images/textview_spellcheck_screenshot_2.png
Binary files differ
diff --git a/docs/html/resources/articles/index.jd b/docs/html/resources/articles/index.jd
index 220a4ed..2947e4a 100644
--- a/docs/html/resources/articles/index.jd
+++ b/docs/html/resources/articles/index.jd
@@ -47,7 +47,16 @@
<dt><a href="{@docRoot}resources/articles/glsurfaceview.html">Introducing GLSurfaceView</a></dt>
<dd>This article provides an overview of GLSurfaceView, a class that makes it easy to implement 2D or 3D OpenGL rendering inside of an Android application.</dd>
</dl>
-
+<dl>
+ <dt>
+ <a href="{@docRoot}resources/articles/spell-checker-framework.jd">
+ Using the Spell Checker Framework</a>
+ </dt>
+ <dd>
+ This article describes how to use the Spell Checker Framework to check spelling in
+ various ways in your application.
+ </dd>
+</dl>
<dl>
<dt><a href="{@docRoot}resources/articles/layout-tricks-reuse.html">Layout Tricks: Creating Reusable UI Components</a></dt>
<dd>Learn how to combine multiple standard UI widgets into a single high-level component, which can be reused throughout your application.</dd>
@@ -149,7 +158,7 @@
</dl>
<dl>
- <dt><a href="{@docRoot}resources/articles/window-bg-speed.html">Window Backgrounds & UI Speed</a></dt>
+ <dt><a href="{@docRoot}resources/articles/window-bg-speed.html">Window Backgrounds & UI Speed</a></dt>
<dd>Some Android applications need to squeeze every bit of performance out of the UI toolkit and there are many ways to do so. In this article, you will discover how to speed up the drawing and the perceived startup time of your activities. Both of these techniques rely on a single feature, the window's background drawable.</dd>
</dl>
diff --git a/docs/html/resources/articles/spell-checker-framework.jd b/docs/html/resources/articles/spell-checker-framework.jd
new file mode 100644
index 0000000..8d57b4e
--- /dev/null
+++ b/docs/html/resources/articles/spell-checker-framework.jd
@@ -0,0 +1,236 @@
+page.title=Using the Spell Checker Framework
+parent.title=Articles
+parent.link=../browser.html?tag=article
+@jd:body
+<div id="qv-wrapper">
+<div id="qv">
+<h2>In This Document</h2>
+<ol>
+ <li>
+ <a href="#SpellCheckLifeCycle">Spell Check Lifecycle</a>
+ </li>
+ <li>
+ <a href="#SpellCheckImplementation">Implementing a Spell Checker Service</a>
+ </li>
+ <li>
+ <a href="#SpellCheckClient">Implementing a Spell Checker Client</a>
+ </li>
+</ol>
+ <h2>See also</h2>
+ <ol>
+ <li>
+ <a href="{@docRoot}resources/samples/SpellChecker/SampleSpellCheckerService/index.html">
+ Spell Checker Service</a> sample app
+ </li>
+ <li>
+ <a href="{@docRoot}resources/samples/SpellChecker/HelloSpellChecker/index.html">
+ Spell Checker Client</a> sample app
+ </li>
+ </ol>
+</div>
+</div>
+
+<p>
+ The Android platform offers a spell checker framework that lets you implement
+ and access spell checking in your application. The framework is one of the
+ Text Service APIs offered by the Android platform.
+</p>
+<p>
+ To use the framework in your app, you create a special type of Android service that
+ generates a spell checker <strong>session</strong> object. Based on text you provide,
+ the session object returns spelling suggestions generated by the spell checker.
+</p>
+<h2 id="SpellCheckLifeCycle">Spell Checker Lifecycle</h2>
+<p>
+ The following diagram shows the lifecycle of the spell checker service:
+</p>
+<img src="{@docRoot}resources/articles/images/spellcheck_lifecycle.png" alt="" height="596"
+ id="figure1" />
+<p class="img-caption">
+ <strong>Figure 1.</strong> The spell checker service lifecycle.
+</p>
+<p>
+ To initiate spell checking, your app starts its implementation of the spell checker
+ service. Clients in your app, such as activities or individual UI elements, request a
+ spell checker session from the service, then use the session to get suggestions for text.
+ As a client terminates its operation, it closes its spell checker session. If necessary, your
+ app can shut down the spell checker service at any time.
+</p>
+<h2 id="SpellCheckImplementation">Implementing a Spell Checker Service</h2>
+<p>
+ To use the spell checker framework in your app, add a spell checker service component including
+ the session object definition. You can also add to your app an optional activity that
+ controls settings. You must also add an XML metadata file that describes
+ the spell checker service, and add the appropriate elements to your manifest file.
+</p>
+<h3 id="SpellCheckCode">Spell checker classes</h3>
+<p>
+ Define the service and session object with the following classes:
+</p>
+<dl>
+ <dt>
+ A subclass of {@link android.service.textservice.SpellCheckerService}
+ </dt>
+ <dd>
+ The {@link android.service.textservice.SpellCheckerService} implements both the
+ {@link android.app.Service} class and the spell checker framework interface. Within your
+ subclass, you must implement the following method:
+ <dl>
+ <dt>{@link android.service.textservice.SpellCheckerService#createSession()}</dt>
+ <dd>
+ A factory method that returns a
+ {@link android.service.textservice.SpellCheckerService.Session} object to a
+ client that wants to do spell checking.
+ </dd>
+ </dl>
+ <p>
+ See the
+ <a href="{@docRoot}resources/samples/SpellChecker/SampleSpellCheckerService/index.html">
+ Spell Checker Service</a> sample app to learn more about implementing this class.
+ </p>
+ </dd>
+ <dt>
+ An implementation of {@link android.service.textservice.SpellCheckerService.Session}
+ </dt>
+ <dd>
+ An object that the spell checker service provides to clients, to let them pass text to
+ the spell checker and receive suggestions. Within this class, you must implement the
+ following methods:
+ <dl>
+ <dt>
+ {@link android.service.textservice.SpellCheckerService.Session#onCreate()}
+ </dt>
+ <dd>
+ Called by the system in response to
+ {@link android.service.textservice.SpellCheckerService#createSession()}. In this
+ method, you can initialize the
+ {@link android.service.textservice.SpellCheckerService.Session} object based on
+ the current locale and so forth.
+ </dd>
+ <dt>
+ {@link android.service.textservice.SpellCheckerService.Session#onGetSuggestions(TextInfo, int)
+ onGetSuggestions()}
+ </dt>
+ <dd>
+ Does the actual spell checking. This method returns an object containing
+ suggestions for the text passed to it.
+ </dd>
+ </dl>
+ <p>
+ Optionally, you can implement
+ {@link android.service.textservice.SpellCheckerService.Session#onCancel()}, which
+ handles requests to cancel spell checking, or
+{@link android.service.textservice.SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)
+onGetSuggestionsMultiple()}, which handles batches of suggestion requests, or both.
+ </p>
+ <p>
+ See the
+ <a href="{@docRoot}resources/samples/SpellChecker/HelloSpellChecker/index.html">
+ Spell Checker Client</a> sample app to learn more about implementing this class.
+ </p>
+ </dd>
+</dl>
+<p class="note">
+ <strong>Note:</strong> You must implement all aspects of spell checking as asynchronous and
+ thread-safe. A spell checker may be called simultaneously by different threads running on
+ different cores. The {@link android.service.textservice.SpellCheckerService} and
+ {@link android.service.textservice.SpellCheckerService.Session} take care of this
+ automatically.
+</p>
+<h3 id="SpellCheckXML">Spell checker manifest and metadata</h3>
+<p>
+ In addition to code, you need to provide the appropriate manifest file and a metadata file for
+ the spell checker.
+</p>
+<p>
+ The manifest file defines the application, the service, and the activity for controlling
+ settings, as shown in the following snippet:
+</p>
+<pre>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.samplespellcheckerservice" >
+ <application
+ android:label="@string/app_name" >
+ <service
+ android:label="@string/app_name"
+ android:name=".SampleSpellCheckerService"
+ android:permission="android.permission.BIND_TEXT_SERVICE" >
+ <intent-filter >
+ <action android:name="android.service.textservice.SpellCheckerService" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.view.textservice.scs"
+ android:resource="@xml/spellchecker" />
+ </service>
+
+ <activity
+ android:label="@string/sample_settings"
+ android:name="SpellCheckerSettingsActivity" >
+ <intent-filter >
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
+</pre>
+<p>
+ Notice that components that want to use the service must request the permission
+ {@link android.Manifest.permission#BIND_TEXT_SERVICE} to ensure that only the system binds to
+ the service. The service's definition also specifies the <code>spellchecker.xml</code> metadata
+ file, which is described in the next section.
+</p>
+<p>
+ The metadata file <code>spellchecker.xml</code> contains the following XML:
+</p>
+<pre>
+<spell-checker xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="@string/spellchecker_name"
+ android:settingsActivity="com.example.SpellCheckerSettingsActivity">
+ <subtype
+ android:label="@string/subtype_generic"
+ android:subtypeLocale="en”
+ />
+ <subtype
+ android:label="@string/subtype_generic"
+ android:subtypeLocale="fr”
+ />
+</spell-checker>
+</pre>
+<p>
+ The metadata specifies the activity that the spell checker uses for controlling settings. It
+ also defines subtypes for the spell checker; in this case, the subtypes define locales that
+ the spell checker can handle.
+</p>
+
+
+<!-- Accessing the Spell Checker Service from a Client -->
+<h2 id="SpellCheckClient">Accessing the Spell Checker Service from a Client</h2>
+<p>
+ Applications that use {@link android.widget.TextView} views automatically benefit from spell
+ checking, because {@link android.widget.TextView} automatically uses a spell checker. The
+ following screenshots show this:
+</p>
+<img src="{@docRoot}resources/articles/images/textview_spellcheck_screenshot_1.png" alt=""
+ height="45" id="figure2a" />
+<br>
+<img src="{@docRoot}resources/articles/images/textview_spellcheck_screenshot_2.png" alt=""
+ height="121" id="figure2b" />
+<p class="img-caption">
+ <strong>Figure 2.</strong> Spell checking in TextView.
+</p>
+<p>
+ However, you may want to interact directly with a spell checker service in other cases as well.
+ The following diagram shows the flow of control for interacting with a spell checker service:
+</p>
+<img src="{@docRoot}resources/articles/images/spellcheck_client_flow.png" alt=""
+ height="394" id="figure3" />
+<p class="img-caption">
+ <strong>Figure 3.</strong> Interacting with a spell checker service.
+</p>
+<p>
+ The <a href="{@docRoot}resources/samples/SpellChecker/HelloSpellChecker/index.html">
+ Spell Checker Client</a> sample app shows how to interact with a spell checker service. The
+ LatinIME input method editor in the Android Open Source Project also contains an example of
+ spell checking.
+</p>
\ No newline at end of file
diff --git a/docs/html/resources/resources-data.js b/docs/html/resources/resources-data.js
index 8ad970b..0b82aee 100644
--- a/docs/html/resources/resources-data.js
+++ b/docs/html/resources/resources-data.js
@@ -263,6 +263,17 @@
}
},
{
+ tags: ['article', 'input', 'ui'],
+ path: 'articles/spell-checker-framework.html',
+ title: {
+ en: 'The Android Spell Checker Framework'
+ },
+ description: {
+ en: 'This article describes the Android spell checker framework and how to use to implement spell checking in applications.'
+ }
+ },
+
+ {
tags: ['article', 'ui'],
path: 'articles/touch-mode.html',
title: {
diff --git a/libs/hwui/DisplayListRenderer.h b/libs/hwui/DisplayListRenderer.h
index 4a9886b..fff1d7c 100644
--- a/libs/hwui/DisplayListRenderer.h
+++ b/libs/hwui/DisplayListRenderer.h
@@ -51,7 +51,7 @@
// Set to 1 to enable native processing of View properties. 0 by default. Eventually this
// will go away and we will always use this approach for accelerated apps.
-#define USE_DISPLAY_LIST_PROPERTIES 1
+#define USE_DISPLAY_LIST_PROPERTIES 0
#define TRANSLATION 0x0001
#define ROTATION 0x0002
diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java
index 860c452..2f4ed89 100644
--- a/media/java/android/media/MediaScanner.java
+++ b/media/java/android/media/MediaScanner.java
@@ -839,7 +839,7 @@
// and EXIF local time is not less than 1 Day, otherwise MediaProvider
// will use file time as taken time.
time = exif.getDateTime();
- if (Math.abs(mLastModified * 1000 - time) >= 86400000) {
+ if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
values.put(Images.Media.DATE_TAKEN, time);
}
}
@@ -1183,7 +1183,7 @@
static class MediaBulkDeleter {
StringBuilder whereClause = new StringBuilder();
- ArrayList<String> whereArgs = new ArrayList<String>(100);
+ ArrayList<String> whereArgs = new ArrayList<String>(100);
IContentProvider mProvider;
Uri mBaseUri;
diff --git a/opengl/java/android/opengl/GLSurfaceView.java b/opengl/java/android/opengl/GLSurfaceView.java
index f69fc53..c937a09 100644
--- a/opengl/java/android/opengl/GLSurfaceView.java
+++ b/opengl/java/android/opengl/GLSurfaceView.java
@@ -779,8 +779,7 @@
if (LOG_THREADS) {
Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId());
}
- throw new RuntimeException("eglDestroyContext failed: "
- + EGLLogWrapper.getErrorString(egl.eglGetError()));
+ EglHelper.throwEglException("eglDestroyContex", egl.eglGetError());
}
}
}
@@ -1094,7 +1093,12 @@
* the context is current and bound to a surface.
*/
if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
- throwEglException("eglMakeCurrent");
+ /*
+ * Could not make the context current, probably because the underlying
+ * SurfaceView surface has been destroyed.
+ */
+ logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
+ return false;
}
return true;
@@ -1130,36 +1134,13 @@
/**
* Display the current render surface.
- * @return false if the context has been lost.
+ * @return the EGL error code from eglSwapBuffers.
*/
- public boolean swap() {
+ public int swap() {
if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
-
- /*
- * Check for EGL_CONTEXT_LOST, which means the context
- * and all associated data were lost (For instance because
- * the device went to sleep). We need to sleep until we
- * get a new surface.
- */
- int error = mEgl.eglGetError();
- switch(error) {
- case EGL11.EGL_CONTEXT_LOST:
- return false;
- case EGL10.EGL_BAD_CURRENT_SURFACE:
- // The current surface is bad, probably because the window manager has closed
- // the associated window. Ignore this error, on the assumption that the
- // application will be closed soon.
- break;
- case EGL10.EGL_BAD_NATIVE_WINDOW:
- // The native window is bad, probably because the window manager has closed it.
- // Ignore this error, on the assumption that the application will be closed
- // soon.
- break;
- default:
- throwEglException("eglSwapBuffers", error);
- }
+ return mEgl.eglGetError();
}
- return true;
+ return EGL10.EGL_SUCCESS;
}
public void destroySurface() {
@@ -1203,14 +1184,23 @@
throwEglException(function, mEgl.eglGetError());
}
- private void throwEglException(String function, int error) {
- String message = function + " failed: " + EGLLogWrapper.getErrorString(error);
+ public static void throwEglException(String function, int error) {
+ String message = formatEglError(function, error);
if (LOG_THREADS) {
- Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + message);
+ Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " "
+ + message);
}
throw new RuntimeException(message);
}
+ public static void logEglErrorAsWarning(String tag, String function, int error) {
+ Log.w(tag, formatEglError(function, error));
+ }
+
+ public static String formatEglError(String function, int error) {
+ return function + " failed: " + EGLLogWrapper.getErrorString(error);
+ }
+
private WeakReference<GLSurfaceView> mGLSurfaceViewWeakRef;
EGL10 mEgl;
EGLDisplay mEglDisplay;
@@ -1357,7 +1347,7 @@
}
}
- // Have we lost the surface view surface?
+ // Have we lost the SurfaceView surface?
if ((! mHasSurface) && (! mWaitingForSurface)) {
if (LOG_SURFACE) {
Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId());
@@ -1366,6 +1356,7 @@
stopEglSurfaceLocked();
}
mWaitingForSurface = true;
+ mSurfaceIsBad = false;
sGLThreadManager.notifyAll();
}
@@ -1423,7 +1414,9 @@
h = mHeight;
wantRenderNotification = true;
if (LOG_SURFACE) {
- Log.i("GLThread", "noticing that we want render notification tid=" + getId());
+ Log.i("GLThread",
+ "noticing that we want render notification tid="
+ + getId());
}
// Destroy and recreate the EGL surface.
@@ -1444,6 +1437,7 @@
+ " mHaveEglSurface: " + mHaveEglSurface
+ " mPaused: " + mPaused
+ " mHasSurface: " + mHasSurface
+ + " mSurfaceIsBad: " + mSurfaceIsBad
+ " mWaitingForSurface: " + mWaitingForSurface
+ " mWidth: " + mWidth
+ " mHeight: " + mHeight
@@ -1465,8 +1459,8 @@
Log.w("GLThread", "egl createSurface");
}
if (!mEglHelper.createSurface()) {
- // Couldn't create a surface. Quit quietly.
- break;
+ mSurfaceIsBad = true;
+ continue;
}
createEglSurface = false;
}
@@ -1509,11 +1503,24 @@
view.mRenderer.onDrawFrame(gl);
}
}
- if (!mEglHelper.swap()) {
- if (LOG_SURFACE) {
- Log.i("GLThread", "egl context lost tid=" + getId());
- }
- lostEglContext = true;
+ int swapError = mEglHelper.swap();
+ switch (swapError) {
+ case EGL10.EGL_SUCCESS:
+ break;
+ case EGL11.EGL_CONTEXT_LOST:
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "egl context lost tid=" + getId());
+ }
+ lostEglContext = true;
+ break;
+ default:
+ // Other errors typically mean that the current surface is bad,
+ // probably because the SurfaceView surface has been destroyed,
+ // but we haven't been notified yet.
+ // Log the error to help developers understand why rendering stopped.
+ EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError);
+ mSurfaceIsBad = true;
+ break;
}
if (wantRenderNotification) {
@@ -1537,7 +1544,7 @@
}
private boolean readyToDraw() {
- return (!mPaused) && mHasSurface
+ return (!mPaused) && mHasSurface && (!mSurfaceIsBad)
&& (mWidth > 0) && (mHeight > 0)
&& (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY));
}
@@ -1707,6 +1714,7 @@
private boolean mRequestPaused;
private boolean mPaused;
private boolean mHasSurface;
+ private boolean mSurfaceIsBad;
private boolean mWaitingForSurface;
private boolean mHaveEglContext;
private boolean mHaveEglSurface;
diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml
index abbc89a..c307c6e 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_row.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml
@@ -1,4 +1,4 @@
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_height"
>
@@ -7,31 +7,17 @@
android:id="@+id/veto"
android:layout_width="48dp"
android:layout_height="match_parent"
- android:layout_centerVertical="true"
- android:layout_alignParentRight="true"
+ android:gravity="right"
android:layout_marginRight="-80dp"
android:background="@null"
android:paddingRight="8dp"
android:paddingLeft="8dp"
/>
- <ImageView
- android:id="@+id/large_icon"
- android:layout_width="@android:dimen/notification_large_icon_width"
- android:layout_height="@android:dimen/notification_large_icon_height"
- android:layout_alignParentTop="true"
- android:layout_alignParentLeft="true"
- android:scaleType="center"
- android:clickable="true"
- android:background="@*android:drawable/notify_panel_notification_icon_bg_tile"
- />
-
<com.android.systemui.statusbar.LatestItemView android:id="@+id/content"
android:layout_width="match_parent"
- android:layout_height="64dp"
- android:layout_alignParentTop="true"
- android:layout_toRightOf="@id/large_icon"
- android:layout_alignParentRight="true"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/notification_divider_height"
android:focusable="true"
android:clickable="true"
android:background="@drawable/notification_row_bg"
@@ -40,8 +26,8 @@
<View
android:layout_width="match_parent"
android:layout_height="@dimen/notification_divider_height"
- android:layout_alignParentBottom="true"
+ android:gravity="bottom"
android:background="@drawable/status_bar_notification_row_background_color"
/>
-</RelativeLayout>
+</FrameLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java b/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java
index 564b07b..ebed522 100644
--- a/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java
@@ -456,6 +456,9 @@
mPreloadTasksRunnable = new Runnable() {
public void run() {
+ // If we set our visibility to INVISIBLE here, we avoid an extra call to
+ // onLayout later when we become visible (because onLayout is always called
+ // when going from GONE)
if (!mShowing) {
setVisibility(INVISIBLE);
refreshRecentTasksList();
@@ -562,9 +565,6 @@
if (!mShowing) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_DOWN) {
- // If we set our visibility to INVISIBLE here, we avoid an extra call to
- // onLayout later when we become visible (because onLayout is always called
- // when going from GONE)
post(mPreloadTasksRunnable);
} else if (action == MotionEvent.ACTION_CANCEL) {
setVisibility(GONE);
@@ -583,9 +583,15 @@
return false;
}
+ public void preloadRecentTasksList() {
+ if (!mShowing) {
+ mPreloadTasksRunnable.run();
+ }
+ }
+
public void clearRecentTasksList() {
// Clear memory used by screenshots
- if (mRecentTaskDescriptions != null) {
+ if (!mShowing && mRecentTaskDescriptions != null) {
mRecentTasksLoader.cancelLoadingThumbnailsAndIcons();
mRecentTaskDescriptions.clear();
mListAdapter.notifyDataSetInvalidated();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 3a06127..23222f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -19,31 +19,53 @@
import java.util.ArrayList;
import android.content.Context;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import android.util.Slog;
import android.view.Display;
import android.view.IWindowManager;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+import android.widget.LinearLayout;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarIconList;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.systemui.SystemUI;
+import com.android.systemui.recent.RecentsPanelView;
+import com.android.systemui.recent.RecentTasksLoader;
+import com.android.systemui.recent.TaskDescription;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.tablet.StatusBarPanel;
import com.android.systemui.R;
-public abstract class BaseStatusBar extends SystemUI implements CommandQueue.Callbacks {
+public abstract class BaseStatusBar extends SystemUI implements
+ CommandQueue.Callbacks, RecentsPanelView.OnRecentsPanelVisibilityChangedListener {
static final String TAG = "StatusBar";
private static final boolean DEBUG = false;
+ protected static final int MSG_OPEN_RECENTS_PANEL = 1020;
+ protected static final int MSG_CLOSE_RECENTS_PANEL = 1021;
+ protected static final int MSG_PRELOAD_RECENT_APPS = 1022;
+ protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023;
+
protected CommandQueue mCommandQueue;
protected IStatusBarService mBarService;
+ protected H mHandler = createHandler();
+
+ // Recent apps
+ protected RecentsPanelView mRecentsPanel;
+ protected RecentTasksLoader mRecentTasksLoader;
// UI-specific methods
@@ -162,4 +184,121 @@
public void dismissIntruder() {
// pass
}
+
+ @Override
+ public void toggleRecentApps() {
+ int msg = (mRecentsPanel.getVisibility() == View.VISIBLE)
+ ? MSG_CLOSE_RECENTS_PANEL : MSG_OPEN_RECENTS_PANEL;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessage(msg);
+ }
+
+ @Override
+ public void preloadRecentApps() {
+ int msg = MSG_PRELOAD_RECENT_APPS;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessage(msg);
+ }
+
+ @Override
+ public void cancelPreloadRecentApps() {
+ int msg = MSG_CANCEL_PRELOAD_RECENT_APPS;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessage(msg);
+ }
+
+ @Override
+ public void onRecentsPanelVisibilityChanged(boolean visible) {
+ }
+
+ protected abstract WindowManager.LayoutParams getRecentsLayoutParams(
+ LayoutParams layoutParams);
+
+ protected void updateRecentsPanel() {
+ // Recents Panel
+ boolean visible = false;
+ ArrayList<TaskDescription> recentTasksList = null;
+ boolean firstScreenful = false;
+ if (mRecentsPanel != null) {
+ visible = mRecentsPanel.isShowing();
+ WindowManagerImpl.getDefault().removeView(mRecentsPanel);
+ if (visible) {
+ recentTasksList = mRecentsPanel.getRecentTasksList();
+ firstScreenful = mRecentsPanel.getFirstScreenful();
+ }
+ }
+
+ // Provide RecentsPanelView with a temporary parent to allow layout params to work.
+ LinearLayout tmpRoot = new LinearLayout(mContext);
+ mRecentsPanel = (RecentsPanelView) LayoutInflater.from(mContext).inflate(
+ R.layout.status_bar_recent_panel, tmpRoot, false);
+ mRecentsPanel.setRecentTasksLoader(mRecentTasksLoader);
+ mRecentTasksLoader.setRecentsPanel(mRecentsPanel);
+ mRecentsPanel.setOnTouchListener(
+ new TouchOutsideListener(MSG_CLOSE_RECENTS_PANEL, mRecentsPanel));
+ mRecentsPanel.setVisibility(View.GONE);
+
+
+ WindowManager.LayoutParams lp = getRecentsLayoutParams(mRecentsPanel.getLayoutParams());
+
+ WindowManagerImpl.getDefault().addView(mRecentsPanel, lp);
+ mRecentsPanel.setBar(this);
+ if (visible) {
+ mRecentsPanel.show(true, false, recentTasksList, firstScreenful);
+ }
+
+ }
+
+ protected H createHandler() {
+ return new H();
+ }
+
+ protected class H extends Handler {
+ public void handleMessage(Message m) {
+ switch (m.what) {
+ case MSG_OPEN_RECENTS_PANEL:
+ if (DEBUG) Slog.d(TAG, "opening recents panel");
+ if (mRecentsPanel != null) {
+ mRecentsPanel.show(true, true);
+ }
+ break;
+ case MSG_CLOSE_RECENTS_PANEL:
+ if (DEBUG) Slog.d(TAG, "closing recents panel");
+ if (mRecentsPanel != null && mRecentsPanel.isShowing()) {
+ mRecentsPanel.show(false, true);
+ }
+ break;
+ case MSG_PRELOAD_RECENT_APPS:
+ if (DEBUG) Slog.d(TAG, "preloading recents");
+ mRecentsPanel.preloadRecentTasksList();
+ break;
+ case MSG_CANCEL_PRELOAD_RECENT_APPS:
+ if (DEBUG) Slog.d(TAG, "cancel preloading recents");
+ mRecentsPanel.clearRecentTasksList();
+ break;
+ }
+ }
+ }
+
+ public class TouchOutsideListener implements View.OnTouchListener {
+ private int mMsg;
+ private StatusBarPanel mPanel;
+
+ public TouchOutsideListener(int msg, StatusBarPanel panel) {
+ mMsg = msg;
+ mPanel = panel;
+ }
+
+ public boolean onTouch(View v, MotionEvent ev) {
+ final int action = ev.getAction();
+ if (action == MotionEvent.ACTION_OUTSIDE
+ || (action == MotionEvent.ACTION_DOWN
+ && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) {
+ mHandler.removeMessages(mMsg);
+ mHandler.sendEmptyMessage(mMsg);
+ return true;
+ }
+ return false;
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index f8dfa8f..37ab58a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -61,8 +61,10 @@
private static final int MSG_SET_HARD_KEYBOARD_STATUS = 10 << MSG_SHIFT;
private static final int MSG_TOGGLE_RECENT_APPS = 11 << MSG_SHIFT;
+ private static final int MSG_PRELOAD_RECENT_APPS = 12 << MSG_SHIFT;
+ private static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 13 << MSG_SHIFT;
- private static final int MSG_SET_NAVIGATION_ICON_HINTS = 13 << MSG_SHIFT;
+ private static final int MSG_SET_NAVIGATION_ICON_HINTS = 14 << MSG_SHIFT;
private StatusBarIconList mList;
private Callbacks mCallbacks;
@@ -92,6 +94,8 @@
public void setImeWindowStatus(IBinder token, int vis, int backDisposition);
public void setHardKeyboardStatus(boolean available, boolean enabled);
public void toggleRecentApps();
+ public void preloadRecentApps();
+ public void cancelPreloadRecentApps();
public void setNavigationIconHints(int hints);
}
@@ -199,6 +203,20 @@
}
}
+ public void preloadRecentApps() {
+ synchronized (mList) {
+ mHandler.removeMessages(MSG_PRELOAD_RECENT_APPS);
+ mHandler.obtainMessage(MSG_PRELOAD_RECENT_APPS, 0, 0, null).sendToTarget();
+ }
+ }
+
+ public void cancelPreloadRecentApps() {
+ synchronized (mList) {
+ mHandler.removeMessages(MSG_CANCEL_PRELOAD_RECENT_APPS);
+ mHandler.obtainMessage(MSG_CANCEL_PRELOAD_RECENT_APPS, 0, 0, null).sendToTarget();
+ }
+ }
+
public void setNavigationIconHints(int hints) {
synchronized (mList) {
mHandler.removeMessages(MSG_SET_NAVIGATION_ICON_HINTS);
@@ -275,6 +293,12 @@
case MSG_TOGGLE_RECENT_APPS:
mCallbacks.toggleRecentApps();
break;
+ case MSG_PRELOAD_RECENT_APPS:
+ mCallbacks.preloadRecentApps();
+ break;
+ case MSG_CANCEL_PRELOAD_RECENT_APPS:
+ mCallbacks.cancelPreloadRecentApps();
+ break;
case MSG_SET_NAVIGATION_ICON_HINTS:
mCallbacks.setNavigationIconHints(msg.arg1);
break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 85e0a8a..7c679b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -30,24 +30,22 @@
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
import android.content.res.Configuration;
-import android.inputmethodservice.InputMethodService;
+import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
+import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.Handler;
import android.os.Message;
+import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
-import android.util.Slog;
import android.util.Log;
+import android.util.Slog;
import android.view.Display;
import android.view.Gravity;
import android.view.IWindowManager;
@@ -76,19 +74,16 @@
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarNotification;
-
import com.android.systemui.R;
import com.android.systemui.SwipeHelper;
import com.android.systemui.recent.RecentTasksLoader;
-import com.android.systemui.recent.RecentsPanelView;
-import com.android.systemui.recent.TaskDescription;
-import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.BaseStatusBar;
-import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.SignalClusterView;
-import com.android.systemui.statusbar.policy.DateView;
+import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.IntruderAlertView;
+import com.android.systemui.statusbar.policy.DateView;
import com.android.systemui.statusbar.policy.LocationController;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.NotificationRowLayout;
@@ -116,8 +111,7 @@
private static final int MSG_CLOSE_NOTIFICATION_PANEL = 1001;
private static final int MSG_SHOW_INTRUDER = 1002;
private static final int MSG_HIDE_INTRUDER = 1003;
- private static final int MSG_OPEN_RECENTS_PANEL = 1020;
- private static final int MSG_CLOSE_RECENTS_PANEL = 1021;
+ // 1020-1030 reserved for BaseStatusBar
// will likely move to a resource or other tunable param at some point
private static final int INTRUDER_ALERT_DECAY_MS = 0; // disabled, was 10000;
@@ -152,7 +146,6 @@
PhoneStatusBarView mStatusBarView;
int mPixelFormat;
- H mHandler = new H();
Object mQueueLock = new Object();
// icons
@@ -202,10 +195,6 @@
private View mTickerView;
private boolean mTicking;
- // Recent apps
- private RecentsPanelView mRecentsPanel;
- private RecentTasksLoader mRecentTasksLoader;
-
// Tracking finger for opening/closing.
int mEdgeBorder; // corresponds to R.dimen.status_bar_edge_ignore
boolean mTracking;
@@ -382,6 +371,7 @@
return sb;
}
+ @Override
protected WindowManager.LayoutParams getRecentsLayoutParams(LayoutParams layoutParams) {
boolean opaque = false;
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
@@ -406,42 +396,13 @@
return lp;
}
+ @Override
protected void updateRecentsPanel() {
- // Recents Panel
- boolean visible = false;
- ArrayList<TaskDescription> recentTasksList = null;
- boolean firstScreenful = false;
- if (mRecentsPanel != null) {
- visible = mRecentsPanel.isShowing();
- WindowManagerImpl.getDefault().removeView(mRecentsPanel);
- if (visible) {
- recentTasksList = mRecentsPanel.getRecentTasksList();
- firstScreenful = mRecentsPanel.getFirstScreenful();
- }
- }
-
- // Provide RecentsPanelView with a temporary parent to allow layout params to work.
- LinearLayout tmpRoot = new LinearLayout(mContext);
- mRecentsPanel = (RecentsPanelView) LayoutInflater.from(mContext).inflate(
- R.layout.status_bar_recent_panel, tmpRoot, false);
- mRecentsPanel.setRecentTasksLoader(mRecentTasksLoader);
- mRecentTasksLoader.setRecentsPanel(mRecentsPanel);
- mRecentsPanel.setOnTouchListener(new TouchOutsideListener(MSG_CLOSE_RECENTS_PANEL,
- mRecentsPanel));
- mRecentsPanel.setVisibility(View.GONE);
-
+ super.updateRecentsPanel();
// Make .03 alpha the minimum so you always see the item a bit-- slightly below
// .03, the item disappears entirely (as if alpha = 0) and that discontinuity looks
// a bit jarring
mRecentsPanel.setMinSwipeAlpha(0.03f);
- WindowManager.LayoutParams lp = getRecentsLayoutParams(mRecentsPanel.getLayoutParams());
-
- WindowManagerImpl.getDefault().addView(mRecentsPanel, lp);
- mRecentsPanel.setBar(this);
- if (visible) {
- mRecentsPanel.show(true, false, recentTasksList, firstScreenful);
- }
-
}
protected int getStatusBarGravity() {
@@ -675,10 +636,8 @@
if (contentIntent != null) {
final View.OnClickListener listener = new NotificationClicker(contentIntent,
notification.pkg, notification.tag, notification.id);
- oldEntry.largeIcon.setOnClickListener(listener);
oldEntry.content.setOnClickListener(listener);
} else {
- oldEntry.largeIcon.setOnClickListener(null);
oldEntry.content.setOnClickListener(null);
}
// Update the icon.
@@ -690,13 +649,6 @@
handleNotificationError(key, notification, "Couldn't update icon: " + ic);
return;
}
- // Update the large icon
- if (notification.notification.largeIcon != null) {
- oldEntry.largeIcon.setImageBitmap(notification.notification.largeIcon);
- } else {
- oldEntry.largeIcon.getLayoutParams().width = 0;
- oldEntry.largeIcon.setVisibility(View.INVISIBLE);
- }
}
catch (RuntimeException e) {
// It failed to add cleanly. Log, and remove the view from the panel.
@@ -878,7 +830,10 @@
private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) {
StatusBarNotification sbn = entry.notification;
- RemoteViews remoteViews = sbn.notification.contentView;
+ // XXX: temporary: while testing big notifications, auto-expand all of them
+ final boolean big = (sbn.notification.bigContentView != null);
+ RemoteViews remoteViews = big ? sbn.notification.bigContentView
+ : sbn.notification.contentView;
if (remoteViews == null) {
return false;
}
@@ -887,20 +842,18 @@
LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
View row = inflater.inflate(R.layout.status_bar_notification_row, parent, false);
+ ViewGroup.LayoutParams lp = row.getLayoutParams();
+ if (big) {
+ lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ } else {
+ lp.height = mContext.getResources().getDimensionPixelSize(R.dimen.notification_height);
+ }
+ row.setLayoutParams(lp);
View vetoButton = updateNotificationVetoButton(row, sbn);
vetoButton.setContentDescription(mContext.getString(
R.string.accessibility_remove_notification));
- // the large icon
- ImageView largeIcon = (ImageView)row.findViewById(R.id.large_icon);
- if (sbn.notification.largeIcon != null) {
- largeIcon.setImageBitmap(sbn.notification.largeIcon);
- largeIcon.setContentDescription(sbn.notification.tickerText);
- } else {
- largeIcon.getLayoutParams().width = 0;
- largeIcon.setVisibility(View.INVISIBLE);
- }
- largeIcon.setContentDescription(sbn.notification.tickerText);
+ // NB: the large icon is now handled entirely by the template
// bind the click event to the content area
ViewGroup content = (ViewGroup)row.findViewById(R.id.content);
@@ -911,10 +864,8 @@
if (contentIntent != null) {
final View.OnClickListener listener = new NotificationClicker(contentIntent,
sbn.pkg, sbn.tag, sbn.id);
- largeIcon.setOnClickListener(listener);
content.setOnClickListener(listener);
} else {
- largeIcon.setOnClickListener(null);
content.setOnClickListener(null);
}
@@ -940,7 +891,6 @@
entry.row = row;
entry.content = content;
entry.expanded = expanded;
- entry.largeIcon = largeIcon;
return true;
}
@@ -1087,11 +1037,17 @@
}
}
+ @Override
+ protected BaseStatusBar.H createHandler() {
+ return new PhoneStatusBar.H();
+ }
+
/**
* All changes to the status bar and notifications funnel through here and are batched.
*/
- private class H extends Handler {
+ private class H extends BaseStatusBar.H {
public void handleMessage(Message m) {
+ super.handleMessage(m);
switch (m.what) {
case MSG_ANIMATE:
doAnimation();
@@ -1112,18 +1068,6 @@
setIntruderAlertVisibility(false);
mCurrentlyIntrudingNotification = null;
break;
- case MSG_OPEN_RECENTS_PANEL:
- if (DEBUG) Slog.d(TAG, "opening recents panel");
- if (mRecentsPanel != null) {
- mRecentsPanel.show(true, true);
- }
- break;
- case MSG_CLOSE_RECENTS_PANEL:
- if (DEBUG) Slog.d(TAG, "closing recents panel");
- if (mRecentsPanel != null && mRecentsPanel.isShowing()) {
- mRecentsPanel.show(false, true);
- }
- break;
}
}
}
@@ -2052,13 +1996,6 @@
}
}
- public void toggleRecentApps() {
- int msg = (mRecentsPanel.getVisibility() == View.VISIBLE)
- ? MSG_CLOSE_RECENTS_PANEL : MSG_OPEN_RECENTS_PANEL;
- mHandler.removeMessages(msg);
- mHandler.sendEmptyMessage(msg);
- }
-
/**
* The LEDs are turned o)ff when the notification panel is shown, even just a little bit.
* This was added last-minute and is inconsistent with the way the rest of the notifications
@@ -2346,27 +2283,5 @@
vibrate();
}
};
-
- public class TouchOutsideListener implements View.OnTouchListener {
- private int mMsg;
- private RecentsPanelView mPanel;
-
- public TouchOutsideListener(int msg, RecentsPanelView panel) {
- mMsg = msg;
- mPanel = panel;
- }
-
- public boolean onTouch(View v, MotionEvent ev) {
- final int action = ev.getAction();
- if (action == MotionEvent.ACTION_OUTSIDE
- || (action == MotionEvent.ACTION_DOWN
- && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) {
- mHandler.removeMessages(mMsg);
- mHandler.sendEmptyMessage(mMsg);
- return true;
- }
- return false;
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java
index 87eb9cc..2491d18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java
@@ -36,21 +36,19 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.inputmethodservice.InputMethodService;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
+import android.inputmethodservice.InputMethodService;
import android.os.Build;
-import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.text.TextUtils;
import android.util.Slog;
-import android.view.accessibility.AccessibilityEvent;
import android.view.Display;
import android.view.Gravity;
import android.view.IWindowManager;
@@ -62,8 +60,10 @@
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.view.WindowManagerImpl;
+import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RemoteViews;
@@ -78,7 +78,6 @@
import com.android.systemui.statusbar.BaseStatusBar;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.SignalClusterView;
-import com.android.systemui.statusbar.BaseStatusBar;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BluetoothController;
@@ -100,8 +99,7 @@
public static final int MSG_CLOSE_NOTIFICATION_PANEL = 1001;
public static final int MSG_OPEN_NOTIFICATION_PEEK = 1002;
public static final int MSG_CLOSE_NOTIFICATION_PEEK = 1003;
- public static final int MSG_OPEN_RECENTS_PANEL = 1020;
- public static final int MSG_CLOSE_RECENTS_PANEL = 1021;
+ // 1020-1029 reserved for BaseStatusBar
public static final int MSG_SHOW_CHROME = 1030;
public static final int MSG_HIDE_CHROME = 1031;
public static final int MSG_OPEN_INPUT_METHODS_PANEL = 1040;
@@ -127,8 +125,6 @@
int mMenuNavIconWidth = -1;
private int mMaxNotificationIcons = 5;
- H mHandler = new H();
-
IWindowManager mWindowManager;
// tracking all current notifications
@@ -189,8 +185,6 @@
// for disabling the status bar
int mDisabled = 0;
- private RecentsPanelView mRecentsPanel;
- private RecentTasksLoader mRecentTasksLoader;
private InputMethodsPanel mInputMethodsPanel;
private CompatModePanel mCompatModePanel;
@@ -348,33 +342,7 @@
// Recents Panel
mRecentTasksLoader = new RecentTasksLoader(context);
- mRecentsPanel = (RecentsPanelView) View.inflate(context,
- R.layout.status_bar_recent_panel, null);
- mRecentsPanel.setVisibility(View.GONE);
- mRecentsPanel.setOnTouchListener(new TouchOutsideListener(MSG_CLOSE_RECENTS_PANEL,
- mRecentsPanel));
- mRecentsPanel.setOnVisibilityChangedListener(this);
- mRecentsPanel.setRecentTasksLoader(mRecentTasksLoader);
- mRecentTasksLoader.setRecentsPanel(mRecentsPanel);
-
- lp = new WindowManager.LayoutParams(
- (int) res.getDimension(R.dimen.status_bar_recents_width),
- ViewGroup.LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
- WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
- | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
- | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
- PixelFormat.TRANSLUCENT);
- lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
- lp.setTitle("RecentsPanel");
- lp.windowAnimations = R.style.Animation_RecentPanel;
- lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED
- | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
-
- WindowManagerImpl.getDefault().addView(mRecentsPanel, lp);
- mRecentsPanel.setBar(this);
- mRecentsPanel.setStatusBarView(mStatusBarView);
+ updateRecentsPanel();
// Input methods Panel
mInputMethodsPanel = (InputMethodsPanel) View.inflate(context,
@@ -680,6 +648,31 @@
return sb;
}
+ @Override
+ protected WindowManager.LayoutParams getRecentsLayoutParams(LayoutParams layoutParams) {
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ (int) mContext.getResources().getDimension(R.dimen.status_bar_recents_width),
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ PixelFormat.TRANSLUCENT);
+ lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
+ lp.setTitle("RecentsPanel");
+ lp.windowAnimations = com.android.internal.R.style.Animation_RecentApplications;
+ lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED
+ | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+
+ return lp;
+ }
+
+ protected void updateRecentsPanel() {
+ super.updateRecentsPanel();
+ mRecentsPanel.setStatusBarView(mStatusBarView);
+ }
+
public int getStatusBarHeight() {
return mHeightReceiver.getHeight();
}
@@ -702,8 +695,14 @@
}
}
- private class H extends Handler {
+ @Override
+ protected BaseStatusBar.H createHandler() {
+ return new TabletStatusBar.H();
+ }
+
+ private class H extends BaseStatusBar.H {
public void handleMessage(Message m) {
+ super.handleMessage(m);
switch (m.what) {
case MSG_OPEN_NOTIFICATION_PEEK:
if (DEBUG) Slog.d(TAG, "opening notification peek window; arg=" + m.arg1);
@@ -798,18 +797,6 @@
mNotificationArea.setVisibility(View.VISIBLE);
}
break;
- case MSG_OPEN_RECENTS_PANEL:
- if (DEBUG) Slog.d(TAG, "opening recents panel");
- if (mRecentsPanel != null) {
- mRecentsPanel.show(true, true);
- }
- break;
- case MSG_CLOSE_RECENTS_PANEL:
- if (DEBUG) Slog.d(TAG, "closing recents panel");
- if (mRecentsPanel != null && mRecentsPanel.isShowing()) {
- mRecentsPanel.show(false, true);
- }
- break;
case MSG_OPEN_INPUT_METHODS_PANEL:
if (DEBUG) Slog.d(TAG, "opening input methods panel");
if (mInputMethodsPanel != null) mInputMethodsPanel.openPanel();
@@ -936,10 +923,8 @@
if (contentIntent != null) {
final View.OnClickListener listener = new NotificationClicker(contentIntent,
notification.pkg, notification.tag, notification.id);
- oldEntry.largeIcon.setOnClickListener(listener);
oldEntry.content.setOnClickListener(listener);
} else {
- oldEntry.largeIcon.setOnClickListener(null);
oldEntry.content.setOnClickListener(null);
}
// Update the icon.
@@ -951,13 +936,6 @@
handleNotificationError(key, notification, "Couldn't update icon: " + ic);
return;
}
- // Update the large icon
- if (notification.notification.largeIcon != null) {
- oldEntry.largeIcon.setImageBitmap(notification.notification.largeIcon);
- } else {
- oldEntry.largeIcon.getLayoutParams().width = 0;
- oldEntry.largeIcon.setVisibility(View.INVISIBLE);
- }
if (NOTIFICATION_PEEK_ENABLED && key == mNotificationPeekKey) {
// must update the peek window
@@ -1860,16 +1838,7 @@
vetoButton.setContentDescription(mContext.getString(
R.string.accessibility_remove_notification));
- // the large icon
- ImageView largeIcon = (ImageView)row.findViewById(R.id.large_icon);
- if (sbn.notification.largeIcon != null) {
- largeIcon.setImageBitmap(sbn.notification.largeIcon);
- largeIcon.setContentDescription(sbn.notification.tickerText);
- } else {
- largeIcon.getLayoutParams().width = 0;
- largeIcon.setVisibility(View.INVISIBLE);
- }
- largeIcon.setContentDescription(sbn.notification.tickerText);
+ // NB: the large icon is now handled entirely by the template
// bind the click event to the content area
ViewGroup content = (ViewGroup)row.findViewById(R.id.content);
@@ -1880,10 +1849,8 @@
if (contentIntent != null) {
final View.OnClickListener listener = new NotificationClicker(
contentIntent, sbn.pkg, sbn.tag, sbn.id);
- largeIcon.setOnClickListener(listener);
content.setOnClickListener(listener);
} else {
- largeIcon.setOnClickListener(null);
content.setOnClickListener(null);
}
@@ -1909,7 +1876,6 @@
entry.row = row;
entry.content = content;
entry.expanded = expanded;
- entry.largeIcon = largeIcon;
return true;
}
@@ -1942,13 +1908,6 @@
visibilityChanged(false);
}
- public void toggleRecentApps() {
- int msg = (mRecentsPanel.getVisibility() == View.VISIBLE)
- ? MSG_CLOSE_RECENTS_PANEL : MSG_OPEN_RECENTS_PANEL;
- mHandler.removeMessages(msg);
- mHandler.sendEmptyMessage(msg);
- }
-
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
@@ -1974,28 +1933,6 @@
}
};
- public class TouchOutsideListener implements View.OnTouchListener {
- private int mMsg;
- private StatusBarPanel mPanel;
-
- public TouchOutsideListener(int msg, StatusBarPanel panel) {
- mMsg = msg;
- mPanel = panel;
- }
-
- public boolean onTouch(View v, MotionEvent ev) {
- final int action = ev.getAction();
- if (action == MotionEvent.ACTION_OUTSIDE
- || (action == MotionEvent.ACTION_DOWN
- && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) {
- mHandler.removeMessages(mMsg);
- mHandler.sendEmptyMessage(mMsg);
- return true;
- }
- return false;
- }
- }
-
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.print("mDisabled=0x");
pw.println(Integer.toHexString(mDisabled));
diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
index 92c94a9..6214086 100755
--- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
+++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
@@ -430,6 +430,7 @@
boolean mHideLockScreen;
boolean mDismissKeyguard;
boolean mHomePressed;
+ boolean mHomeLongPressed;
Intent mHomeIntent;
Intent mCarDockIntent;
Intent mDeskDockIntent;
@@ -744,7 +745,7 @@
// Eat the longpress so it won't dismiss the recent apps dialog when
// the user lets go of the home key
- mHomePressed = false;
+ mHomeLongPressed = true;
}
if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_RECENT_DIALOG) {
@@ -1619,33 +1620,45 @@
// it handle it, because that gives us the correct 5 second
// timeout.
if (keyCode == KeyEvent.KEYCODE_HOME) {
+
// If we have released the home key, and didn't do anything else
// while it was pressed, then it is time to go home!
- if (mHomePressed && !down) {
+ if (!down) {
+ final boolean homeWasLongPressed = mHomeLongPressed;
mHomePressed = false;
- if (!canceled) {
- // If an incoming call is ringing, HOME is totally disabled.
- // (The user is already on the InCallScreen at this point,
- // and his ONLY options are to answer or reject the call.)
- boolean incomingRinging = false;
+ mHomeLongPressed = false;
+ if (!homeWasLongPressed) {
try {
- ITelephony telephonyService = getTelephonyService();
- if (telephonyService != null) {
- incomingRinging = telephonyService.isRinging();
- }
- } catch (RemoteException ex) {
- Log.w(TAG, "RemoteException from getPhoneInterface()", ex);
+ mStatusBarService.cancelPreloadRecentApps();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException when showing recent apps", e);
}
- if (incomingRinging) {
- Log.i(TAG, "Ignoring HOME; there's a ringing incoming call.");
+ mHomePressed = false;
+ if (!canceled) {
+ // If an incoming call is ringing, HOME is totally disabled.
+ // (The user is already on the InCallScreen at this point,
+ // and his ONLY options are to answer or reject the call.)
+ boolean incomingRinging = false;
+ try {
+ ITelephony telephonyService = getTelephonyService();
+ if (telephonyService != null) {
+ incomingRinging = telephonyService.isRinging();
+ }
+ } catch (RemoteException ex) {
+ Log.w(TAG, "RemoteException from getPhoneInterface()", ex);
+ }
+
+ if (incomingRinging) {
+ Log.i(TAG, "Ignoring HOME; there's a ringing incoming call.");
+ } else {
+ launchHomeFromHotKey();
+ }
} else {
- launchHomeFromHotKey();
+ Log.i(TAG, "Ignoring HOME; event canceled.");
}
- } else {
- Log.i(TAG, "Ignoring HOME; event canceled.");
+ return -1;
}
- return -1;
}
// If a system window has focus, then it doesn't make sense
@@ -1666,8 +1679,14 @@
}
}
}
-
if (down) {
+ if (!mHomePressed) {
+ try {
+ mStatusBarService.preloadRecentApps();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException when preloading recent apps", e);
+ }
+ }
if (repeatCount == 0) {
mHomePressed = true;
} else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java
index 624ee98..6833a57 100755
--- a/services/java/com/android/server/NotificationManagerService.java
+++ b/services/java/com/android/server/NotificationManagerService.java
@@ -79,6 +79,7 @@
private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION;
+ private static final boolean SCORE_ONGOING_HIGHER = false;
final Context mContext;
final IActivityManager mAm;
@@ -720,7 +721,7 @@
// Migrate notification flags to scores
if (0 != (notification.flags & Notification.FLAG_HIGH_PRIORITY)) {
if (notification.priority < Notification.PRIORITY_MAX) notification.priority = Notification.PRIORITY_MAX;
- } else if (0 != (notification.flags & Notification.FLAG_ONGOING_EVENT)) {
+ } else if (SCORE_ONGOING_HIGHER && 0 != (notification.flags & Notification.FLAG_ONGOING_EVENT)) {
if (notification.priority < Notification.PRIORITY_HIGH) notification.priority = Notification.PRIORITY_HIGH;
}
diff --git a/services/java/com/android/server/StatusBarManagerService.java b/services/java/com/android/server/StatusBarManagerService.java
index a9ff6c5..6452be7 100644
--- a/services/java/com/android/server/StatusBarManagerService.java
+++ b/services/java/com/android/server/StatusBarManagerService.java
@@ -352,6 +352,24 @@
}
}
+ @Override
+ public void preloadRecentApps() {
+ if (mBar != null) {
+ try {
+ mBar.preloadRecentApps();
+ } catch (RemoteException ex) {}
+ }
+ }
+
+ @Override
+ public void cancelPreloadRecentApps() {
+ if (mBar != null) {
+ try {
+ mBar.cancelPreloadRecentApps();
+ } catch (RemoteException ex) {}
+ }
+ }
+
private void enforceStatusBar() {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR,
"StatusBarManagerService");
diff --git a/services/java/com/android/server/usb/UsbDeviceManager.java b/services/java/com/android/server/usb/UsbDeviceManager.java
index c2ded8a..4bea5e4 100644
--- a/services/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/java/com/android/server/usb/UsbDeviceManager.java
@@ -591,6 +591,7 @@
notification.defaults = 0; // please be quiet
notification.sound = null;
notification.vibrate = null;
+ notification.priority = Notification.PRIORITY_MIN;
Intent intent = Intent.makeRestartActivityTask(
new ComponentName("com.android.settings",
@@ -624,6 +625,7 @@
notification.defaults = 0; // please be quiet
notification.sound = null;
notification.vibrate = null;
+ notification.priority = Notification.PRIORITY_MIN;
Intent intent = Intent.makeRestartActivityTask(
new ComponentName("com.android.settings",
diff --git a/services/java/com/android/server/wm/AppWindowToken.java b/services/java/com/android/server/wm/AppWindowToken.java
index 6269420..d1f1b44 100644
--- a/services/java/com/android/server/wm/AppWindowToken.java
+++ b/services/java/com/android/server/wm/AppWindowToken.java
@@ -184,13 +184,14 @@
final int adj = animLayerAdjustment;
thumbnailLayer = -1;
for (int i=0; i<N; i++) {
- WindowState w = allAppWindows.get(i);
- w.mAnimLayer = w.mLayer + adj;
- if (w.mAnimLayer > thumbnailLayer) {
- thumbnailLayer = w.mAnimLayer;
+ final WindowState w = allAppWindows.get(i);
+ final WindowStateAnimator winAnimator = w.mWinAnimator;
+ winAnimator.mAnimLayer = w.mLayer + adj;
+ if (winAnimator.mAnimLayer > thumbnailLayer) {
+ thumbnailLayer = winAnimator.mAnimLayer;
}
if (WindowManagerService.DEBUG_LAYERS) Slog.v(WindowManagerService.TAG, "Updating layer " + w + ": "
- + w.mAnimLayer);
+ + winAnimator.mAnimLayer);
if (w == service.mInputMethodTarget && !service.mInputMethodTargetWaitingAnim) {
service.setInputMethodAnimLayerAdjustment(adj);
}
@@ -221,11 +222,11 @@
boolean isAnimating = false;
final int NW = allAppWindows.size();
for (int i=0; i<NW; i++) {
- WindowState w = allAppWindows.get(i);
+ WindowStateAnimator winAnimator = allAppWindows.get(i).mWinAnimator;
if (WindowManagerService.DEBUG_VISIBILITY) Slog.v(WindowManagerService.TAG,
- "performing show on: " + w);
- w.performShowLocked();
- isAnimating |= w.mWinAnimator.isAnimating();
+ "performing show on: " + winAnimator);
+ winAnimator.performShowLocked();
+ isAnimating |= winAnimator.isAnimating();
}
return isAnimating;
}
@@ -390,10 +391,10 @@
+ win.isDrawnLw()
+ ", isAnimating=" + win.mWinAnimator.isAnimating());
if (!win.isDrawnLw()) {
- Slog.v(WindowManagerService.TAG, "Not displayed: s=" + win.mSurface
+ Slog.v(WindowManagerService.TAG, "Not displayed: s=" + win.mWinAnimator.mSurface
+ " pv=" + win.mPolicyVisibility
- + " dp=" + win.mDrawPending
- + " cdp=" + win.mCommitDrawPending
+ + " dp=" + win.mWinAnimator.mDrawPending
+ + " cdp=" + win.mWinAnimator.mCommitDrawPending
+ " ah=" + win.mAttachedHidden
+ " th="
+ (win.mAppToken != null
diff --git a/services/java/com/android/server/wm/DimAnimator.java b/services/java/com/android/server/wm/DimAnimator.java
index d8f2254..0051d98 100644
--- a/services/java/com/android/server/wm/DimAnimator.java
+++ b/services/java/com/android/server/wm/DimAnimator.java
@@ -85,17 +85,18 @@
* {@link #updateSurface} after all windows are examined.
*/
void updateParameters(Resources res, WindowState w, long currentTime) {
- mDimSurface.setLayer(w.mAnimLayer - WindowManagerService.LAYER_OFFSET_DIM);
+ final WindowStateAnimator winAnimator = w.mWinAnimator;
+ mDimSurface.setLayer(winAnimator.mAnimLayer - WindowManagerService.LAYER_OFFSET_DIM);
final float target = w.mExiting ? 0 : w.mAttrs.dimAmount;
- if (WindowManagerService.SHOW_TRANSACTIONS) Slog.i(WindowManagerService.TAG, " DIM " + mDimSurface
- + ": layer=" + (w.mAnimLayer-1) + " target=" + target);
+ if (WindowManagerService.SHOW_TRANSACTIONS) Slog.i(WindowManagerService.TAG, " DIM "
+ + mDimSurface + ": layer=" + (winAnimator.mAnimLayer-1) + " target=" + target);
if (mDimTargetAlpha != target) {
// If the desired dim level has changed, then
// start an animation to it.
mLastDimAnimTime = currentTime;
- long duration = (w.mWinAnimator.mAnimating && w.mWinAnimator.mAnimation != null)
- ? w.mWinAnimator.mAnimation.computeDurationHint()
+ long duration = (winAnimator.mAnimating && winAnimator.mAnimation != null)
+ ? winAnimator.mAnimation.computeDurationHint()
: WindowManagerService.DEFAULT_DIM_DURATION;
if (target > mDimTargetAlpha) {
TypedValue tv = new TypedValue();
diff --git a/services/java/com/android/server/wm/WindowAnimator.java b/services/java/com/android/server/wm/WindowAnimator.java
index 81e0a17..26f4d0d 100644
--- a/services/java/com/android/server/wm/WindowAnimator.java
+++ b/services/java/com/android/server/wm/WindowAnimator.java
@@ -4,19 +4,22 @@
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+import static com.android.server.wm.WindowManagerService.LayoutFields.SET_UPDATE_ROTATION;
+import static com.android.server.wm.WindowManagerService.LayoutFields.SET_WALLPAPER_MAY_CHANGE;
+
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import android.util.Slog;
import android.view.Surface;
import android.view.WindowManager;
-import android.view.WindowManager.LayoutParams;
import android.view.WindowManagerPolicy;
import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
import com.android.internal.policy.impl.PhoneWindowManager;
+import java.io.PrintWriter;
+
/**
* @author cmautner@google.com (Craig Mautner)
* Singleton class that carries out the animations and Surface operations in a separate task
@@ -30,7 +33,6 @@
final WindowManagerPolicy mPolicy;
boolean mAnimating;
- boolean mUpdateRotation;
boolean mTokenMayBeDrawn;
boolean mForceHiding;
WindowState mWindowAnimationBackground;
@@ -55,6 +57,16 @@
/** The one and only screen rotation if one is happening */
ScreenRotationAnimation mScreenRotationAnimation = null;
+ // Window currently running an animation that has requested it be detached
+ // from the wallpaper. This means we need to ensure the wallpaper is
+ // visible behind it in case it animates in a way that would allow it to be
+ // seen.
+ WindowState mWindowDetachedWallpaper = null;
+ WindowState mDetachedWallpaper = null;
+ DimSurface mWindowAnimationBackgroundSurface = null;
+
+ int mBulkUpdateParams = 0;
+
WindowAnimator(final WindowManagerService service, final Context context,
final WindowManagerPolicy policy) {
mService = service;
@@ -62,6 +74,44 @@
mPolicy = policy;
}
+ private void testWallpaperAndBackgroundLocked() {
+ if (mWindowDetachedWallpaper != mDetachedWallpaper) {
+ if (WindowManagerService.DEBUG_WALLPAPER) Slog.v(TAG,
+ "Detached wallpaper changed from " + mWindowDetachedWallpaper
+ + " to " + mDetachedWallpaper);
+ mWindowDetachedWallpaper = mDetachedWallpaper;
+ mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
+ }
+
+ if (mWindowAnimationBackgroundColor != 0) {
+ // If the window that wants black is the current wallpaper
+ // target, then the black goes *below* the wallpaper so we
+ // don't cause the wallpaper to suddenly disappear.
+ WindowState target = mWindowAnimationBackground;
+ if (mService.mWallpaperTarget == target
+ || mService.mLowerWallpaperTarget == target
+ || mService.mUpperWallpaperTarget == target) {
+ for (int i=0; i<mService.mWindows.size(); i++) {
+ WindowState w = mService.mWindows.get(i);
+ if (w.mIsWallpaper) {
+ target = w;
+ break;
+ }
+ }
+ }
+ if (mWindowAnimationBackgroundSurface == null) {
+ mWindowAnimationBackgroundSurface = new DimSurface(mService.mFxSession);
+ }
+ final int dw = mDw;
+ final int dh = mDh;
+ mWindowAnimationBackgroundSurface.show(dw, dh,
+ target.mWinAnimator.mAnimLayer - WindowManagerService.LAYER_OFFSET_DIM,
+ mWindowAnimationBackgroundColor);
+ } else if (mWindowAnimationBackgroundSurface != null) {
+ mWindowAnimationBackgroundSurface.hide();
+ }
+ }
+
private void updateWindowsAppsAndRotationAnimationsLocked() {
int i;
final int NAT = mService.mAppTokens.size();
@@ -100,10 +150,9 @@
(mScreenRotationAnimation.isAnimating() ||
mScreenRotationAnimation.mFinishAnimReady)) {
if (mScreenRotationAnimation.stepAnimationLocked(mCurrentTime)) {
- mUpdateRotation = false;
mAnimating = true;
} else {
- mUpdateRotation = true;
+ mBulkUpdateParams |= SET_UPDATE_ROTATION;
mScreenRotationAnimation.kill();
mScreenRotationAnimation = null;
}
@@ -116,42 +165,9 @@
for (int i = mService.mWindows.size() - 1; i >= 0; i--) {
WindowState w = mService.mWindows.get(i);
WindowStateAnimator winAnimator = w.mWinAnimator;
-
final WindowManager.LayoutParams attrs = w.mAttrs;
- if (w.mSurface != null) {
- // Take care of the window being ready to display.
- if (w.commitFinishDrawingLocked(mCurrentTime)) {
- if ((w.mAttrs.flags
- & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
- if (WindowManagerService.DEBUG_WALLPAPER) Slog.v(TAG,
- "First draw done in potential wallpaper target " + w);
- mService.mInnerFields.mWallpaperMayChange = true;
- mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
- if (WindowManagerService.DEBUG_LAYOUT_REPEATS) {
- mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 1");
- }
- }
- }
-
- // If the window has moved due to its containing
- // content frame changing, then we'd like to animate
- // it. The checks here are ordered by what is least
- // likely to be true first.
- if (w.shouldAnimateMove()) {
- // Frame has moved, containing content frame
- // has also moved, and we're not currently animating...
- // let's do something.
- Animation a = AnimationUtils.loadAnimation(mContext,
- com.android.internal.R.anim.window_move_from_decor);
- winAnimator.setAnimation(a);
- w.mAnimDw = w.mLastFrame.left - w.mFrame.left;
- w.mAnimDh = w.mLastFrame.top - w.mFrame.top;
- } else {
- w.mAnimDw = mInnerDw;
- w.mAnimDh = mInnerDh;
- }
-
+ if (winAnimator.mSurface != null) {
final boolean wasAnimating = winAnimator.mWasAnimating;
final boolean nowAnimating = winAnimator.stepAnimationLocked(mCurrentTime);
@@ -165,13 +181,14 @@
// a detached wallpaper animation.
if (nowAnimating) {
if (winAnimator.mAnimation != null) {
- if ((w.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0
+ if ((attrs.flags&FLAG_SHOW_WALLPAPER) != 0
&& winAnimator.mAnimation.getDetachWallpaper()) {
- mService.mInnerFields.mDetachedWallpaper = w;
+ mDetachedWallpaper = w;
}
if (winAnimator.mAnimation.getBackgroundColor() != 0) {
if (mWindowAnimationBackground == null
- || (w.mAnimLayer < mWindowAnimationBackground.mAnimLayer)) {
+ || (winAnimator.mAnimLayer <
+ mWindowAnimationBackground.mWinAnimator.mAnimLayer)) {
mWindowAnimationBackground = w;
mWindowAnimationBackgroundColor =
winAnimator.mAnimation.getBackgroundColor();
@@ -186,14 +203,14 @@
// displayed behind it.
if (w.mAppToken != null && w.mAppToken.animation != null
&& w.mAppToken.animating) {
- if ((w.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0
+ if ((attrs.flags&FLAG_SHOW_WALLPAPER) != 0
&& w.mAppToken.animation.getDetachWallpaper()) {
- mService.mInnerFields.mDetachedWallpaper = w;
+ mDetachedWallpaper = w;
}
if (w.mAppToken.animation.getBackgroundColor() != 0) {
if (mWindowAnimationBackground == null
- || (w.mAnimLayer <
- mWindowAnimationBackground.mAnimLayer)) {
+ || (winAnimator.mAnimLayer <
+ mWindowAnimationBackground.mWinAnimator.mAnimLayer)) {
mWindowAnimationBackground = w;
mWindowAnimationBackgroundColor =
w.mAppToken.animation.getBackgroundColor();
@@ -202,7 +219,7 @@
}
if (wasAnimating && !winAnimator.mAnimating && mService.mWallpaperTarget == w) {
- mService.mInnerFields.mWallpaperMayChange = true;
+ mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
if (WindowManagerService.DEBUG_LAYOUT_REPEATS) {
mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 2");
@@ -255,7 +272,7 @@
}
if (changed && (attrs.flags
& WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
- mService.mInnerFields.mWallpaperMayChange = true;
+ mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
if (WindowManagerService.DEBUG_LAYOUT_REPEATS) {
mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 4");
@@ -280,10 +297,10 @@
+ w.isDrawnLw()
+ ", isAnimating=" + winAnimator.isAnimating());
if (!w.isDrawnLw()) {
- Slog.v(TAG, "Not displayed: s=" + w.mSurface
+ Slog.v(TAG, "Not displayed: s=" + winAnimator.mSurface
+ " pv=" + w.mPolicyVisibility
- + " dp=" + w.mDrawPending
- + " cdp=" + w.mCommitDrawPending
+ + " dp=" + winAnimator.mDrawPending
+ + " cdp=" + winAnimator.mCommitDrawPending
+ " ah=" + w.mAttachedHidden
+ " th=" + atoken.hiddenRequested
+ " a=" + winAnimator.mAnimating);
@@ -307,7 +324,7 @@
}
}
} else if (w.mReadyToShow) {
- if (w.performShowLocked()) {
+ if (winAnimator.performShowLocked()) {
mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
if (WindowManagerService.DEBUG_LAYOUT_REPEATS) {
mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 5");
@@ -319,8 +336,8 @@
atoken.thumbnailTransactionSeq = mTransactionSequence;
atoken.thumbnailLayer = 0;
}
- if (atoken.thumbnailLayer < w.mAnimLayer) {
- atoken.thumbnailLayer = w.mAnimLayer;
+ if (atoken.thumbnailLayer < winAnimator.mAnimLayer) {
+ atoken.thumbnailLayer = winAnimator.mAnimLayer;
}
}
} // end forall windows
@@ -370,14 +387,9 @@
}
private void performAnimationsLocked() {
- if (WindowManagerService.DEBUG_APP_TRANSITIONS) Slog.v(TAG, "*** ANIM STEP: seq="
- + mTransactionSequence + " mAnimating="
- + mAnimating);
-
mTokenMayBeDrawn = false;
- mService.mInnerFields.mWallpaperMayChange = false;
mForceHiding = false;
- mService.mInnerFields.mDetachedWallpaper = null;
+ mDetachedWallpaper = null;
mWindowAnimationBackground = null;
mWindowAnimationBackgroundColor = 0;
@@ -386,198 +398,19 @@
if (mTokenMayBeDrawn) {
testTokenMayBeDrawnLocked();
}
-
- if (WindowManagerService.DEBUG_APP_TRANSITIONS) Slog.v(TAG, "*** ANIM STEP: changes=0x"
- + Integer.toHexString(mPendingLayoutChanges));
- }
-
- public void prepareSurfaceLocked(final WindowState w, final boolean recoveringMemory) {
- if (w.mSurface == null) {
- if (w.mOrientationChanging) {
- if (WindowManagerService.DEBUG_ORIENTATION) {
- Slog.v(TAG, "Orientation change skips hidden " + w);
- }
- w.mOrientationChanging = false;
- }
- return;
- }
-
- boolean displayed = false;
-
- w.computeShownFrameLocked();
-
- int width, height;
- if ((w.mAttrs.flags & LayoutParams.FLAG_SCALED) != 0) {
- // for a scaled surface, we just want to use
- // the requested size.
- width = w.mRequestedWidth;
- height = w.mRequestedHeight;
- } else {
- width = w.mCompatFrame.width();
- height = w.mCompatFrame.height();
- }
-
- if (width < 1) {
- width = 1;
- }
- if (height < 1) {
- height = 1;
- }
- final boolean surfaceResized = w.mSurfaceW != width || w.mSurfaceH != height;
- if (surfaceResized) {
- w.mSurfaceW = width;
- w.mSurfaceH = height;
- }
-
- if (w.mSurfaceX != w.mShownFrame.left
- || w.mSurfaceY != w.mShownFrame.top) {
- try {
- if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
- "POS " + w.mShownFrame.left
- + ", " + w.mShownFrame.top, null);
- w.mSurfaceX = w.mShownFrame.left;
- w.mSurfaceY = w.mShownFrame.top;
- w.mSurface.setPosition(w.mShownFrame.left, w.mShownFrame.top);
- } catch (RuntimeException e) {
- Slog.w(TAG, "Error positioning surface of " + w
- + " pos=(" + w.mShownFrame.left
- + "," + w.mShownFrame.top + ")", e);
- if (!recoveringMemory) {
- mService.reclaimSomeSurfaceMemoryLocked(w, "position", true);
- }
- }
- }
-
- if (surfaceResized) {
- try {
- if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
- "SIZE " + width + "x" + height, null);
- w.mSurfaceResized = true;
- w.mSurface.setSize(width, height);
- } catch (RuntimeException e) {
- // If something goes wrong with the surface (such
- // as running out of memory), don't take down the
- // entire system.
- Slog.e(TAG, "Error resizing surface of " + w
- + " size=(" + width + "x" + height + ")", e);
- if (!recoveringMemory) {
- mService.reclaimSomeSurfaceMemoryLocked(w, "size", true);
- }
- }
- }
-
- if (w.mAttachedHidden || !w.isReadyForDisplay()) {
- if (!w.mLastHidden) {
- //dump();
- w.mLastHidden = true;
- if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
- "HIDE (performLayout)", null);
- if (w.mSurface != null) {
- w.mSurfaceShown = false;
- try {
- w.mSurface.hide();
- } catch (RuntimeException e) {
- Slog.w(TAG, "Exception hiding surface in " + w);
- }
- }
- }
- // If we are waiting for this window to handle an
- // orientation change, well, it is hidden, so
- // doesn't really matter. Note that this does
- // introduce a potential glitch if the window
- // becomes unhidden before it has drawn for the
- // new orientation.
- if (w.mOrientationChanging) {
- w.mOrientationChanging = false;
- if (WindowManagerService.DEBUG_ORIENTATION) Slog.v(TAG,
- "Orientation change skips hidden " + w);
- }
- } else if (w.mLastLayer != w.mAnimLayer
- || w.mLastAlpha != w.mShownAlpha
- || w.mLastDsDx != w.mDsDx
- || w.mLastDtDx != w.mDtDx
- || w.mLastDsDy != w.mDsDy
- || w.mLastDtDy != w.mDtDy
- || w.mLastHScale != w.mHScale
- || w.mLastVScale != w.mVScale
- || w.mLastHidden) {
- displayed = true;
- w.mLastAlpha = w.mShownAlpha;
- w.mLastLayer = w.mAnimLayer;
- w.mLastDsDx = w.mDsDx;
- w.mLastDtDx = w.mDtDx;
- w.mLastDsDy = w.mDsDy;
- w.mLastDtDy = w.mDtDy;
- w.mLastHScale = w.mHScale;
- w.mLastVScale = w.mVScale;
- if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
- "alpha=" + w.mShownAlpha + " layer=" + w.mAnimLayer
- + " matrix=[" + (w.mDsDx*w.mHScale)
- + "," + (w.mDtDx*w.mVScale)
- + "][" + (w.mDsDy*w.mHScale)
- + "," + (w.mDtDy*w.mVScale) + "]", null);
- if (w.mSurface != null) {
- try {
- w.mSurfaceAlpha = w.mShownAlpha;
- w.mSurface.setAlpha(w.mShownAlpha);
- w.mSurfaceLayer = w.mAnimLayer;
- w.mSurface.setLayer(w.mAnimLayer);
- w.mSurface.setMatrix(
- w.mDsDx*w.mHScale, w.mDtDx*w.mVScale,
- w.mDsDy*w.mHScale, w.mDtDy*w.mVScale);
- } catch (RuntimeException e) {
- Slog.w(TAG, "Error updating surface in " + w, e);
- if (!recoveringMemory) {
- mService.reclaimSomeSurfaceMemoryLocked(w, "update", true);
- }
- }
- }
-
- if (w.mLastHidden && w.isDrawnLw()
- && !w.mReadyToShow) {
- if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
- "SHOW (performLayout)", null);
- if (WindowManagerService.DEBUG_VISIBILITY) Slog.v(TAG, "Showing " + w
- + " during relayout");
- if (mService.showSurfaceRobustlyLocked(w)) {
- w.mHasDrawn = true;
- w.mLastHidden = false;
- } else {
- w.mOrientationChanging = false;
- }
- }
- if (w.mSurface != null) {
- w.mToken.hasVisible = true;
- }
- } else {
- displayed = true;
- }
-
- if (displayed) {
- if (w.mOrientationChanging) {
- if (!w.isDrawnLw()) {
- mService.mInnerFields.mOrientationChangeComplete = false;
- if (WindowManagerService.DEBUG_ORIENTATION) Slog.v(TAG,
- "Orientation continue waiting for draw in " + w);
- } else {
- w.mOrientationChanging = false;
- if (WindowManagerService.DEBUG_ORIENTATION) Slog.v(TAG,
- "Orientation change complete in " + w);
- }
- }
- w.mToken.hasVisible = true;
- }
}
void animate() {
mPendingLayoutChanges = 0;
mCurrentTime = SystemClock.uptimeMillis();
+ mBulkUpdateParams = 0;
// Update animations of all applications, including those
// associated with exiting/removed apps
Surface.openTransaction();
try {
+ testWallpaperAndBackgroundLocked();
updateWindowsAppsAndRotationAnimationsLocked();
performAnimationsLocked();
@@ -590,7 +423,7 @@
final int N = mService.mWindows.size();
for (int i=N-1; i>=0; i--) {
WindowState w = mService.mWindows.get(i);
- prepareSurfaceLocked(w, true);
+ w.mWinAnimator.prepareSurfaceLocked(true);
}
if (mService.mDimAnimator != null && mService.mDimAnimator.mDimShown) {
@@ -611,6 +444,10 @@
} finally {
Surface.closeTransaction();
}
+
+ if (mBulkUpdateParams != 0) {
+ mService.bulkSetParameters(mBulkUpdateParams);
+ }
}
WindowState mCurrentFocus;
@@ -626,4 +463,13 @@
mInnerDh = appHeight;
}
+ public void dump(PrintWriter pw, String prefix, boolean dumpAll) {
+ if (mWindowDetachedWallpaper != null) {
+ pw.print(" mWindowDetachedWallpaper="); pw.println(mWindowDetachedWallpaper);
+ }
+ if (mWindowAnimationBackgroundSurface != null) {
+ pw.println(" mWindowAnimationBackgroundSurface:");
+ mWindowAnimationBackgroundSurface.printTo(" ", pw);
+ }
+ }
}
diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java
index 9635b33..a91e716 100644
--- a/services/java/com/android/server/wm/WindowManagerService.java
+++ b/services/java/com/android/server/wm/WindowManagerService.java
@@ -554,12 +554,6 @@
// If non-null, we are in the middle of animating from one wallpaper target
// to another, and this is the higher one in Z-order.
WindowState mUpperWallpaperTarget = null;
- // Window currently running an animation that has requested it be detached
- // from the wallpaper. This means we need to ensure the wallpaper is
- // visible behind it in case it animates in a way that would allow it to be
- // seen.
- WindowState mWindowDetachedWallpaper = null;
- DimSurface mWindowAnimationBackgroundSurface = null;
int mWallpaperAnimLayerAdjustment;
float mLastWallpaperX = -1;
float mLastWallpaperY = -1;
@@ -595,12 +589,14 @@
/** Pulled out of performLayoutAndPlaceSurfacesLockedInner in order to refactor into multiple
* methods. */
- class LayoutAndSurfaceFields {
+ class LayoutFields {
+ static final int SET_UPDATE_ROTATION = 1 << 0;
+ static final int SET_WALLPAPER_MAY_CHANGE = 1 << 1;
+
boolean mWallpaperForceHidingChanged = false;
boolean mWallpaperMayChange = false;
- WindowState mDetachedWallpaper = null;
boolean mOrientationChangeComplete = true;
- private int mAdjResult = 0;
+ int mAdjResult = 0;
private Session mHoldScreen = null;
private boolean mObscured = false;
boolean mDimming = false;
@@ -609,7 +605,7 @@
private float mButtonBrightness = -1;
private boolean mUpdateRotation = false;
}
- LayoutAndSurfaceFields mInnerFields = new LayoutAndSurfaceFields();
+ LayoutFields mInnerFields = new LayoutFields();
/** Only do a maximum of 6 repeated layouts. After that quit */
private int mLayoutRepeatCount;
@@ -1132,7 +1128,8 @@
if (DEBUG_INPUT_METHOD) {
Slog.i(TAG, "isVisibleOrAdding " + w + ": " + w.isVisibleOrAdding());
if (!w.isVisibleOrAdding()) {
- Slog.i(TAG, " mSurface=" + w.mSurface + " reportDestroy=" + w.mReportDestroySurface
+ Slog.i(TAG, " mSurface=" + w.mWinAnimator.mSurface + " reportDestroy="
+ + w.mWinAnimator.mReportDestroySurface
+ " relayoutCalled=" + w.mRelayoutCalled + " viewVis=" + w.mViewVisibility
+ " policyVis=" + w.mPolicyVisibility + " attachHid=" + w.mAttachedHidden
+ " exiting=" + w.mExiting + " destroying=" + w.mDestroying);
@@ -1196,7 +1193,7 @@
if (mInputMethodTarget != null && w != null
&& mInputMethodTarget.isDisplayedLw()
&& mInputMethodTarget.mExiting) {
- if (mInputMethodTarget.mAnimLayer > w.mAnimLayer) {
+ if (mInputMethodTarget.mWinAnimator.mAnimLayer > w.mWinAnimator.mAnimLayer) {
w = mInputMethodTarget;
i = localmWindows.indexOf(w);
if (DEBUG_INPUT_METHOD) Slog.v(TAG, "Current target higher, switching to: " + w);
@@ -1225,8 +1222,8 @@
break;
}
if (!win.mRemoved) {
- if (highestTarget == null || win.mAnimLayer >
- highestTarget.mAnimLayer) {
+ if (highestTarget == null || win.mWinAnimator.mAnimLayer >
+ highestTarget.mWinAnimator.mAnimLayer) {
highestTarget = win;
highestPos = pos;
}
@@ -1239,8 +1236,8 @@
if (DEBUG_INPUT_METHOD) Slog.v(TAG, "mNextAppTransition="
+ mNextAppTransition + " " + highestTarget
+ " animating=" + highestTarget.mWinAnimator.isAnimating()
- + " layer=" + highestTarget.mAnimLayer
- + " new layer=" + w.mAnimLayer);
+ + " layer=" + highestTarget.mWinAnimator.mAnimLayer
+ + " new layer=" + w.mWinAnimator.mAnimLayer);
if (mNextAppTransition != WindowManagerPolicy.TRANSIT_UNSET) {
// If we are currently setting up for an animation,
@@ -1249,7 +1246,7 @@
mInputMethodTarget = highestTarget;
return highestPos + 1;
} else if (highestTarget.mWinAnimator.isAnimating() &&
- highestTarget.mAnimLayer > w.mAnimLayer) {
+ highestTarget.mWinAnimator.mAnimLayer > w.mWinAnimator.mAnimLayer) {
// If the window we are currently targeting is involved
// with an animation, and it is on top of the next target
// we will be over, then hold off on moving until
@@ -1321,25 +1318,25 @@
mInputMethodAnimLayerAdjustment = adj;
WindowState imw = mInputMethodWindow;
if (imw != null) {
- imw.mAnimLayer = imw.mLayer + adj;
+ imw.mWinAnimator.mAnimLayer = imw.mLayer + adj;
if (DEBUG_LAYERS) Slog.v(TAG, "IM win " + imw
- + " anim layer: " + imw.mAnimLayer);
+ + " anim layer: " + imw.mWinAnimator.mAnimLayer);
int wi = imw.mChildWindows.size();
while (wi > 0) {
wi--;
WindowState cw = imw.mChildWindows.get(wi);
- cw.mAnimLayer = cw.mLayer + adj;
+ cw.mWinAnimator.mAnimLayer = cw.mLayer + adj;
if (DEBUG_LAYERS) Slog.v(TAG, "IM win " + cw
- + " anim layer: " + cw.mAnimLayer);
+ + " anim layer: " + cw.mWinAnimator.mAnimLayer);
}
}
int di = mInputMethodDialogs.size();
while (di > 0) {
di --;
imw = mInputMethodDialogs.get(di);
- imw.mAnimLayer = imw.mLayer + adj;
+ imw.mWinAnimator.mAnimLayer = imw.mLayer + adj;
if (DEBUG_LAYERS) Slog.v(TAG, "IM win " + imw
- + " anim layer: " + imw.mAnimLayer);
+ + " anim layer: " + imw.mWinAnimator.mAnimLayer);
}
}
@@ -1554,6 +1551,7 @@
static final int ADJUST_WALLPAPER_VISIBILITY_CHANGED = 1<<2;
int adjustWallpaperWindowsLocked() {
+ mInnerFields.mWallpaperMayChange = false;
int changed = 0;
final int dw = mAppDisplayWidth;
@@ -1581,7 +1579,7 @@
continue;
}
topCurW = null;
- if (w != mWindowDetachedWallpaper && w.mAppToken != null) {
+ if (w != mAnimator.mWindowDetachedWallpaper && w.mAppToken != null) {
// If this window's app token is hidden and not animating,
// it is of no interest to us.
if (w.mAppToken.hidden && w.mAppToken.animation == null) {
@@ -1591,8 +1589,8 @@
}
}
if (DEBUG_WALLPAPER) Slog.v(TAG, "Win " + w + ": readyfordisplay="
- + w.isReadyForDisplay() + " drawpending=" + w.mDrawPending
- + " commitdrawpending=" + w.mCommitDrawPending);
+ + w.isReadyForDisplay() + " drawpending=" + w.mWinAnimator.mDrawPending
+ + " commitdrawpending=" + w.mWinAnimator.mCommitDrawPending);
if ((w.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0 && w.isReadyForDisplay()
&& (mWallpaperTarget == w || w.isDrawnLw())) {
if (DEBUG_WALLPAPER) Slog.v(TAG,
@@ -1610,7 +1608,7 @@
continue;
}
break;
- } else if (w == mWindowDetachedWallpaper) {
+ } else if (w == mAnimator.mWindowDetachedWallpaper) {
windowDetachedI = i;
}
}
@@ -1826,9 +1824,9 @@
}
}
- wallpaper.mAnimLayer = wallpaper.mLayer + mWallpaperAnimLayerAdjustment;
+ wallpaper.mWinAnimator.mAnimLayer = wallpaper.mLayer + mWallpaperAnimLayerAdjustment;
if (DEBUG_LAYERS || DEBUG_WALLPAPER) Slog.v(TAG, "Wallpaper win "
- + wallpaper + " anim layer: " + wallpaper.mAnimLayer);
+ + wallpaper + " anim layer: " + wallpaper.mWinAnimator.mAnimLayer);
// First, if this window is at the current index, then all
// is well.
@@ -1880,9 +1878,9 @@
while (curWallpaperIndex > 0) {
curWallpaperIndex--;
WindowState wallpaper = token.windows.get(curWallpaperIndex);
- wallpaper.mAnimLayer = wallpaper.mLayer + adj;
+ wallpaper.mWinAnimator.mAnimLayer = wallpaper.mLayer + adj;
if (DEBUG_LAYERS || DEBUG_WALLPAPER) Slog.v(TAG, "Wallpaper win "
- + wallpaper + " anim layer: " + wallpaper.mAnimLayer);
+ + wallpaper + " anim layer: " + wallpaper.mWinAnimator.mAnimLayer);
}
}
}
@@ -2001,19 +1999,20 @@
curWallpaperIndex--;
WindowState wallpaper = token.windows.get(curWallpaperIndex);
if (updateWallpaperOffsetLocked(wallpaper, dw, dh, sync)) {
- wallpaper.computeShownFrameLocked();
+ WindowStateAnimator winAnimator = wallpaper.mWinAnimator;
+ winAnimator.computeShownFrameLocked();
// No need to lay out the windows - we can just set the wallpaper position
// directly.
- if (wallpaper.mSurfaceX != wallpaper.mShownFrame.left
- || wallpaper.mSurfaceY != wallpaper.mShownFrame.top) {
+ if (winAnimator.mSurfaceX != wallpaper.mShownFrame.left
+ || winAnimator.mSurfaceY != wallpaper.mShownFrame.top) {
Surface.openTransaction();
try {
if (SHOW_TRANSACTIONS) logSurface(wallpaper,
"POS " + wallpaper.mShownFrame.left
+ ", " + wallpaper.mShownFrame.top, null);
- wallpaper.mSurfaceX = wallpaper.mShownFrame.left;
- wallpaper.mSurfaceY = wallpaper.mShownFrame.top;
- wallpaper.mSurface.setPosition(wallpaper.mShownFrame.left,
+ winAnimator.mSurfaceX = wallpaper.mShownFrame.left;
+ winAnimator.mSurfaceY = wallpaper.mShownFrame.top;
+ winAnimator.mSurface.setPosition(wallpaper.mShownFrame.left,
wallpaper.mShownFrame.top);
} catch (RuntimeException e) {
Slog.w(TAG, "Error positioning surface of " + wallpaper
@@ -2223,7 +2222,7 @@
}
}
- win.mEnterAnimationPending = true;
+ win.mWinAnimator.mEnterAnimationPending = true;
mPolicy.getContentInsetHintLw(attrs, outContentInsets);
@@ -2294,14 +2293,14 @@
TAG, "Remove " + win + " client="
+ Integer.toHexString(System.identityHashCode(
win.mClient.asBinder()))
- + ", surface=" + win.mSurface);
+ + ", surface=" + win.mWinAnimator.mSurface);
final long origId = Binder.clearCallingIdentity();
win.disposeInputChannel();
if (DEBUG_APP_TRANSITIONS) Slog.v(
- TAG, "Remove " + win + ": mSurface=" + win.mSurface
+ TAG, "Remove " + win + ": mSurface=" + win.mWinAnimator.mSurface
+ " mExiting=" + win.mExiting
+ " isAnimating=" + win.mWinAnimator.isAnimating()
+ " app-animation="
@@ -2315,7 +2314,7 @@
// to hold off on removing the window until the animation is done.
// If the display is frozen, just remove immediately, since the
// animation wouldn't be seen.
- if (win.mSurface != null && okToDisplay()) {
+ if (win.mWinAnimator.mSurface != null && okToDisplay()) {
// If we are not currently running the exit animation, we
// need to see about starting one.
wasVisible = win.isWinVisibleLw();
@@ -2326,7 +2325,7 @@
transit = WindowManagerPolicy.TRANSIT_PREVIEW_DONE;
}
// Try starting an animation.
- if (applyAnimationLocked(win, transit, false)) {
+ if (win.mWinAnimator.applyAnimationLocked(transit, false)) {
win.mExiting = true;
}
}
@@ -2482,14 +2481,15 @@
try {
synchronized (mWindowMap) {
WindowState w = windowForClientLocked(session, client, false);
- if ((w != null) && (w.mSurface != null)) {
+ WindowStateAnimator winAnimator = w.mWinAnimator;
+ if ((w != null) && (winAnimator.mSurface != null)) {
if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
">>> OPEN TRANSACTION setTransparentRegion");
Surface.openTransaction();
try {
if (SHOW_TRANSACTIONS) logSurface(w,
"transparentRegionHint=" + region, null);
- w.mSurface.setTransparentRegionHint(region);
+ winAnimator.mSurface.setTransparentRegionHint(region);
} finally {
Surface.closeTransaction();
if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
@@ -2618,6 +2618,7 @@
synchronized(mWindowMap) {
WindowState win = windowForClientLocked(session, client, false);
+ WindowStateAnimator winAnimator = win.mWinAnimator;
if (win == null) {
return 0;
}
@@ -2635,7 +2636,7 @@
mPolicy.adjustWindowParamsLw(attrs);
}
- win.mSurfaceDestroyDeferred =
+ winAnimator.mSurfaceDestroyDeferred =
(flags&WindowManagerImpl.RELAYOUT_DEFER_SURFACE_DESTROY) != 0;
int attrChanges = 0;
@@ -2657,7 +2658,7 @@
win.mEnforceSizeCompat = (win.mAttrs.flags & FLAG_COMPATIBLE_WINDOW) != 0;
if ((attrChanges & WindowManager.LayoutParams.ALPHA_CHANGED) != 0) {
- win.mAlpha = attrs.alpha;
+ winAnimator.mAlpha = attrs.alpha;
}
final boolean scaledWindow =
@@ -2699,18 +2700,18 @@
(win.mAppToken == null || !win.mAppToken.clientHidden)) {
displayed = !win.isVisibleLw();
if (win.mExiting) {
- win.mWinAnimator.cancelExitAnimationForNextAnimationLocked();
+ winAnimator.cancelExitAnimationForNextAnimationLocked();
}
if (win.mDestroying) {
win.mDestroying = false;
mDestroySurface.remove(win);
}
if (oldVisibility == View.GONE) {
- win.mEnterAnimationPending = true;
+ winAnimator.mEnterAnimationPending = true;
}
if (displayed) {
if (win.isDrawnLw() && okToDisplay()) {
- applyEnterAnimationLocked(win);
+ winAnimator.applyEnterAnimationLocked();
}
if ((win.mAttrs.flags
& WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) != 0) {
@@ -2733,19 +2734,19 @@
}
if ((attrChanges&WindowManager.LayoutParams.FORMAT_CHANGED) != 0) {
// To change the format, we need to re-build the surface.
- win.destroySurfaceLocked();
+ winAnimator.destroySurfaceLocked();
displayed = true;
surfaceChanged = true;
}
try {
- if (win.mSurface == null) {
+ if (winAnimator.mSurface == null) {
surfaceChanged = true;
}
- Surface surface = win.createSurfaceLocked();
+ Surface surface = winAnimator.createSurfaceLocked();
if (surface != null) {
outSurface.copyFrom(surface);
- win.mReportDestroySurface = false;
- win.mSurfacePendingDestroy = false;
+ winAnimator.mReportDestroySurface = false;
+ winAnimator.mSurfacePendingDestroy = false;
if (SHOW_TRANSACTIONS) Slog.i(TAG,
" OUT SURFACE " + outSurface + ": copied");
} else {
@@ -2784,14 +2785,14 @@
sa.flags = (sa.flags&~mask) | (win.mAttrs.flags&mask);
}
} else {
- win.mEnterAnimationPending = false;
- if (win.mSurface != null) {
+ winAnimator.mEnterAnimationPending = false;
+ if (winAnimator.mSurface != null) {
if (DEBUG_VISIBILITY) Slog.i(TAG, "Relayout invis " + win
+ ": mExiting=" + win.mExiting
- + " mSurfacePendingDestroy=" + win.mSurfacePendingDestroy);
+ + " mSurfacePendingDestroy=" + winAnimator.mSurfacePendingDestroy);
// If we are not currently running the exit animation, we
// need to see about starting one.
- if (!win.mExiting || win.mSurfacePendingDestroy) {
+ if (!win.mExiting || winAnimator.mSurfacePendingDestroy) {
surfaceChanged = true;
// Try starting an animation; if there isn't one, we
// can destroy the surface right away.
@@ -2799,8 +2800,8 @@
if (win.mAttrs.type == TYPE_APPLICATION_STARTING) {
transit = WindowManagerPolicy.TRANSIT_PREVIEW_DONE;
}
- if (!win.mSurfacePendingDestroy && win.isWinVisibleLw() &&
- applyAnimationLocked(win, transit, false)) {
+ if (!winAnimator.mSurfacePendingDestroy && win.isWinVisibleLw() &&
+ winAnimator.applyAnimationLocked(transit, false)) {
focusMayChange = true;
win.mExiting = true;
} else if (win.mWinAnimator.isAnimating()) {
@@ -2817,26 +2818,26 @@
if (mInputMethodWindow == win) {
mInputMethodWindow = null;
}
- win.destroySurfaceLocked();
+ winAnimator.destroySurfaceLocked();
}
}
}
- if (win.mSurface == null || (win.getAttrs().flags
+ if (winAnimator.mSurface == null || (win.getAttrs().flags
& WindowManager.LayoutParams.FLAG_KEEP_SURFACE_WHILE_ANIMATING) == 0
- || win.mSurfacePendingDestroy) {
+ || winAnimator.mSurfacePendingDestroy) {
// We could be called from a local process, which
// means outSurface holds its current surface. Ensure the
// surface object is cleared, but we don't necessarily want
// it actually destroyed at this point.
- win.mSurfacePendingDestroy = false;
+ winAnimator.mSurfacePendingDestroy = false;
outSurface.release();
if (DEBUG_VISIBILITY) Slog.i(TAG, "Releasing surface in: " + win);
- } else if (win.mSurface != null) {
+ } else if (winAnimator.mSurface != null) {
if (DEBUG_VISIBILITY) Slog.i(TAG,
"Keeping surface, will report destroy: " + win);
- win.mReportDestroySurface = true;
- outSurface.copyFrom(win.mSurface);
+ winAnimator.mReportDestroySurface = true;
+ outSurface.copyFrom(winAnimator.mSurface);
}
}
@@ -2921,7 +2922,7 @@
if (win == null) {
return;
}
- win.destroyDeferredSurfaceLocked();
+ win.mWinAnimator.destroyDeferredSurfaceLocked();
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -2937,7 +2938,7 @@
if (win == null) {
return false;
}
- return reclaimSomeSurfaceMemoryLocked(win, "from-client", false);
+ return reclaimSomeSurfaceMemoryLocked(win.mWinAnimator, "from-client", false);
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -2948,7 +2949,7 @@
final long origId = Binder.clearCallingIdentity();
synchronized(mWindowMap) {
WindowState win = windowForClientLocked(session, client, false);
- if (win != null && win.finishDrawingLocked()) {
+ if (win != null && win.mWinAnimator.finishDrawingLocked()) {
if ((win.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0) {
adjustWallpaperWindowsLocked();
}
@@ -3002,90 +3003,7 @@
return null;
}
- void applyEnterAnimationLocked(WindowState win) {
- final int transit;
- if (win.mEnterAnimationPending) {
- win.mEnterAnimationPending = false;
- transit = WindowManagerPolicy.TRANSIT_ENTER;
- } else {
- transit = WindowManagerPolicy.TRANSIT_SHOW;
- }
-
- applyAnimationLocked(win, transit, true);
- }
-
- /**
- * Choose the correct animation and set it to the passed WindowState.
- * @param win The window to add the animation to.
- * @param transit If WindowManagerPolicy.TRANSIT_PREVIEW_DONE and the app window has been drawn
- * then the animation will be app_starting_exit. Any other value loads the animation from
- * the switch statement below.
- * @param isEntrance The animation type the last time this was called. Used to keep from
- * loading the same animation twice.
- * @return true if an animation has been loaded.
- */
- boolean applyAnimationLocked(WindowState win,
- int transit, boolean isEntrance) {
- if (win.mWinAnimator.mLocalAnimating &&
- win.mWinAnimator.mAnimationIsEntrance == isEntrance) {
- // If we are trying to apply an animation, but already running
- // an animation of the same type, then just leave that one alone.
- return true;
- }
-
- // Only apply an animation if the display isn't frozen. If it is
- // frozen, there is no reason to animate and it can cause strange
- // artifacts when we unfreeze the display if some different animation
- // is running.
- if (okToDisplay()) {
- int anim = mPolicy.selectAnimationLw(win, transit);
- int attr = -1;
- Animation a = null;
- if (anim != 0) {
- a = AnimationUtils.loadAnimation(mContext, anim);
- } else {
- switch (transit) {
- case WindowManagerPolicy.TRANSIT_ENTER:
- attr = com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
- break;
- case WindowManagerPolicy.TRANSIT_EXIT:
- attr = com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
- break;
- case WindowManagerPolicy.TRANSIT_SHOW:
- attr = com.android.internal.R.styleable.WindowAnimation_windowShowAnimation;
- break;
- case WindowManagerPolicy.TRANSIT_HIDE:
- attr = com.android.internal.R.styleable.WindowAnimation_windowHideAnimation;
- break;
- }
- if (attr >= 0) {
- a = loadAnimation(win.mAttrs, attr);
- }
- }
- if (DEBUG_ANIM) Slog.v(TAG, "applyAnimation: win=" + win
- + " anim=" + anim + " attr=0x" + Integer.toHexString(attr)
- + " mAnimation=" + win.mWinAnimator.mAnimation
- + " isEntrance=" + isEntrance);
- if (a != null) {
- if (DEBUG_ANIM) {
- RuntimeException e = null;
- if (!HIDE_STACK_CRAWLS) {
- e = new RuntimeException();
- e.fillInStackTrace();
- }
- Slog.v(TAG, "Loaded animation " + a + " for " + win, e);
- }
- win.mWinAnimator.setAnimation(a);
- win.mWinAnimator.mAnimationIsEntrance = isEntrance;
- }
- } else {
- win.mWinAnimator.clearAnimation();
- }
-
- return win.mWinAnimator.mAnimation != null;
- }
-
- private Animation loadAnimation(WindowManager.LayoutParams lp, int animAttr) {
+ Animation loadAnimation(WindowManager.LayoutParams lp, int animAttr) {
int anim = 0;
Context context = mContext;
if (animAttr >= 0) {
@@ -3376,8 +3294,7 @@
}
if (win.isVisibleNow()) {
- applyAnimationLocked(win,
- WindowManagerPolicy.TRANSIT_EXIT, false);
+ win.mWinAnimator.applyAnimationLocked(WindowManagerPolicy.TRANSIT_EXIT, false);
changed = true;
}
}
@@ -3636,7 +3553,6 @@
* android.os.IBinder)
*/
boolean updateOrientationFromAppTokensLocked(boolean inTransaction) {
- boolean changed = false;
long ident = Binder.clearCallingIdentity();
try {
int req = computeForcedAppOrientationLocked();
@@ -3647,11 +3563,12 @@
//action like disabling/enabling sensors etc.,
mPolicy.setCurrentOrientationLw(req);
if (updateRotationUncheckedLocked(inTransaction)) {
- changed = true;
+ // changed
+ return true;
}
}
- return changed;
+ return false;
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -4073,14 +3990,14 @@
if (visible) {
if (!win.isVisibleNow()) {
if (!runningAppAnimation) {
- applyAnimationLocked(win,
+ win.mWinAnimator.applyAnimationLocked(
WindowManagerPolicy.TRANSIT_ENTER, true);
}
changed = true;
}
} else if (win.isVisibleNow()) {
if (!runningAppAnimation) {
- applyAnimationLocked(win,
+ win.mWinAnimator.applyAnimationLocked(
WindowManagerPolicy.TRANSIT_EXIT, false);
}
changed = true;
@@ -4220,7 +4137,7 @@
WindowState w = wtoken.allAppWindows.get(i);
if (w.mAppFreezing) {
w.mAppFreezing = false;
- if (w.mSurface != null && !w.mOrientationChanging) {
+ if (w.mWinAnimator.mSurface != null && !w.mOrientationChanging) {
if (DEBUG_ORIENTATION) Slog.v(TAG, "set mOrientationChanging of " + w);
w.mOrientationChanging = true;
}
@@ -4808,7 +4725,7 @@
synchronized(mWindowMap) {
for (int i=mWindows.size()-1; i>=0; i--) {
WindowState w = mWindows.get(i);
- if (w.mSurface != null) {
+ if (w.mWinAnimator.mSurface != null) {
try {
w.mClient.closeSystemDialogs(reason);
} catch (RemoteException e) {
@@ -5280,7 +5197,7 @@
boolean including = false;
for (int i=mWindows.size()-1; i>=0; i--) {
WindowState ws = mWindows.get(i);
- if (ws.mSurface == null) {
+ if (ws.mWinAnimator.mSurface == null) {
continue;
}
if (ws.mLayer >= aboveAppLayer) {
@@ -5306,8 +5223,8 @@
// window.
including = !ws.mIsImWindow && !ws.isFullscreen(dw, dh);
- if (maxLayer < ws.mAnimLayer) {
- maxLayer = ws.mAnimLayer;
+ if (maxLayer < ws.mWinAnimator.mAnimLayer) {
+ maxLayer = ws.mWinAnimator.mAnimLayer;
}
// Don't include wallpaper in bounds calculation
@@ -5368,8 +5285,8 @@
Slog.i(TAG, "Screenshot: " + dw + "x" + dh + " from 0 to " + maxLayer);
for (int i=0; i<mWindows.size(); i++) {
Slog.i(TAG, mWindows.get(i) + ": " + mWindows.get(i).mLayer
- + " animLayer=" + mWindows.get(i).mAnimLayer
- + " surfaceLayer=" + mWindows.get(i).mSurfaceLayer);
+ + " animLayer=" + mWindows.get(i).mWinAnimator.mAnimLayer
+ + " surfaceLayer=" + mWindows.get(i).mWinAnimator.mSurfaceLayer);
}
}
rawss = Surface.screenshot(dw, dh, 0, maxLayer);
@@ -5594,7 +5511,7 @@
for (int i=mWindows.size()-1; i>=0; i--) {
WindowState w = mWindows.get(i);
- if (w.mSurface != null) {
+ if (w.mWinAnimator.mSurface != null) {
if (DEBUG_ORIENTATION) Slog.v(TAG, "Set mOrientationChanging of " + w);
w.mOrientationChanging = true;
}
@@ -6739,6 +6656,7 @@
public static final int REPORT_HARD_KEYBOARD_STATUS_CHANGE = 22;
public static final int BOOT_TIMEOUT = 23;
public static final int WAITING_FOR_DRAWN_TIMEOUT = 24;
+ public static final int BULK_UPDATE_PARAMETERS = 25;
private Session mLastReportedHold;
@@ -7149,6 +7067,21 @@
}
break;
}
+
+ case BULK_UPDATE_PARAMETERS: {
+ synchronized (mWindowMap) {
+ // TODO(cmautner): As the number of bits grows, use masks of bit groups to
+ // eliminate unnecessary tests.
+ if ((msg.arg1 & LayoutFields.SET_UPDATE_ROTATION) != 0) {
+ mInnerFields.mUpdateRotation = true;
+ }
+ if ((msg.arg1 & LayoutFields.SET_WALLPAPER_MAY_CHANGE) != 0) {
+ mInnerFields.mWallpaperMayChange = true;
+ }
+
+ requestTraversalLocked();
+ }
+ }
}
}
}
@@ -7495,19 +7428,19 @@
w.mLayer = curLayer;
}
if (w.mTargetAppToken != null) {
- w.mAnimLayer = w.mLayer + w.mTargetAppToken.animLayerAdjustment;
+ w.mWinAnimator.mAnimLayer = w.mLayer + w.mTargetAppToken.animLayerAdjustment;
} else if (w.mAppToken != null) {
- w.mAnimLayer = w.mLayer + w.mAppToken.animLayerAdjustment;
+ w.mWinAnimator.mAnimLayer = w.mLayer + w.mAppToken.animLayerAdjustment;
} else {
- w.mAnimLayer = w.mLayer;
+ w.mWinAnimator.mAnimLayer = w.mLayer;
}
if (w.mIsImWindow) {
- w.mAnimLayer += mInputMethodAnimLayerAdjustment;
+ w.mWinAnimator.mAnimLayer += mInputMethodAnimLayerAdjustment;
} else if (w.mIsWallpaper) {
- w.mAnimLayer += mWallpaperAnimLayerAdjustment;
+ w.mWinAnimator.mAnimLayer += mWallpaperAnimLayerAdjustment;
}
if (DEBUG_LAYERS) Slog.v(TAG, "Assign layer " + w + ": "
- + w.mAnimLayer);
+ + w.mWinAnimator.mAnimLayer);
//System.out.println(
// "Assigned layer " + curLayer + " to " + w.mClient.asBinder());
}
@@ -7944,8 +7877,8 @@
int layer = -1;
for (int j=0; j<wtoken.windows.size(); j++) {
WindowState win = wtoken.windows.get(j);
- if (win.mAnimLayer > layer) {
- layer = win.mAnimLayer;
+ if (win.mWinAnimator.mAnimLayer > layer) {
+ layer = win.mWinAnimator.mAnimLayer;
}
}
if (topOpeningApp == null || layer > topOpeningLayer) {
@@ -8083,7 +8016,6 @@
}
}
mInnerFields.mAdjResult |= adjustWallpaperWindowsLocked();
- mInnerFields.mWallpaperMayChange = false;
mInnerFields.mWallpaperForceHidingChanged = false;
if (DEBUG_WALLPAPER) Slog.v(TAG, "****** OLD: " + oldWallpaper
+ " NEW: " + mWallpaperTarget
@@ -8094,7 +8026,7 @@
mAnimator.mForceHiding = false;
for (int i=mWindows.size()-1; i>=0; i--) {
WindowState w = mWindows.get(i);
- if (w.mSurface != null) {
+ if (w.mWinAnimator.mSurface != null) {
final WindowManager.LayoutParams attrs = w.mAttrs;
if (mPolicy.doesForceHide(w, attrs) && w.isVisibleLw()) {
if (DEBUG_FOCUS) Slog.i(TAG, "win=" + w + " force hides other windows");
@@ -8113,80 +8045,8 @@
return changes;
}
- /**
- * Extracted from {@link #performLayoutAndPlaceSurfacesLockedInner} to reduce size of method.
- *
- * @return bitmap indicating if another pass through layout must be made.
- */
- private int testWallpaperAndBackgroundLocked() {
- int changes = 0;
-
- if (mWindowDetachedWallpaper != mInnerFields.mDetachedWallpaper) {
- if (DEBUG_WALLPAPER) Slog.v(TAG,
- "Detached wallpaper changed from " + mWindowDetachedWallpaper
- + " to " + mInnerFields.mDetachedWallpaper);
- mWindowDetachedWallpaper = mInnerFields.mDetachedWallpaper;
- mInnerFields.mWallpaperMayChange = true;
- }
-
- if (mAnimator.mWindowAnimationBackgroundColor != 0) {
- // If the window that wants black is the current wallpaper
- // target, then the black goes *below* the wallpaper so we
- // don't cause the wallpaper to suddenly disappear.
- WindowState target = mAnimator.mWindowAnimationBackground;
- if (mWallpaperTarget == target
- || mLowerWallpaperTarget == target
- || mUpperWallpaperTarget == target) {
- for (int i=0; i<mWindows.size(); i++) {
- WindowState w = mWindows.get(i);
- if (w.mIsWallpaper) {
- target = w;
- break;
- }
- }
- }
- if (mWindowAnimationBackgroundSurface == null) {
- mWindowAnimationBackgroundSurface = new DimSurface(mFxSession);
- }
- final int dw = mCurDisplayWidth;
- final int dh = mCurDisplayHeight;
- mWindowAnimationBackgroundSurface.show(dw, dh,
- target.mAnimLayer - LAYER_OFFSET_DIM,
- mAnimator.mWindowAnimationBackgroundColor);
- } else if (mWindowAnimationBackgroundSurface != null) {
- mWindowAnimationBackgroundSurface.hide();
- }
-
- if (mInnerFields.mWallpaperMayChange) {
- if (DEBUG_WALLPAPER) Slog.v(TAG,
- "Wallpaper may change! Adjusting");
- mInnerFields.mAdjResult |= adjustWallpaperWindowsLocked();
- }
-
- if ((mInnerFields.mAdjResult&ADJUST_WALLPAPER_LAYERS_CHANGED) != 0) {
- if (DEBUG_WALLPAPER) Slog.v(TAG,
- "Wallpaper layer changed: assigning layers + relayout");
- changes |= PhoneWindowManager.FINISH_LAYOUT_REDO_LAYOUT;
- assignLayersLocked();
- } else if ((mInnerFields.mAdjResult&ADJUST_WALLPAPER_VISIBILITY_CHANGED) != 0) {
- if (DEBUG_WALLPAPER) Slog.v(TAG,
- "Wallpaper visibility changed: relayout");
- changes |= PhoneWindowManager.FINISH_LAYOUT_REDO_LAYOUT;
- }
-
- if (mFocusMayChange) {
- mFocusMayChange = false;
- if (updateFocusedWindowLocked(UPDATE_FOCUS_PLACING_SURFACES,
- false /*updateInputWindows*/)) {
- changes |= PhoneWindowManager.FINISH_LAYOUT_REDO_ANIM;
- mInnerFields.mAdjResult = 0;
- }
- }
-
- return changes;
- }
-
private void updateResizingWindows(final WindowState w) {
+ final WindowStateAnimator winAnimator = w.mWinAnimator;
if (!w.mAppFreezing && w.mLayoutSeq == mLayoutSeq) {
w.mContentInsetsChanged |=
!w.mLastContentInsets.equals(w.mContentInsets);
@@ -8206,13 +8066,13 @@
w.mLastFrame.set(w.mFrame);
if (w.mContentInsetsChanged
|| w.mVisibleInsetsChanged
- || w.mSurfaceResized
+ || winAnimator.mSurfaceResized
|| configChanged) {
if (DEBUG_RESIZE || DEBUG_ORIENTATION) {
Slog.v(TAG, "Resize reasons: "
+ " contentInsetsChanged=" + w.mContentInsetsChanged
+ " visibleInsetsChanged=" + w.mVisibleInsetsChanged
- + " surfaceResized=" + w.mSurfaceResized
+ + " surfaceResized=" + w.mWinAnimator.mSurfaceResized
+ " configChanged=" + configChanged);
}
@@ -8227,9 +8087,9 @@
if (w.mOrientationChanging) {
if (DEBUG_ORIENTATION) Slog.v(TAG,
"Orientation start waiting for draw in "
- + w + ", surface " + w.mSurface);
- w.mDrawPending = true;
- w.mCommitDrawPending = false;
+ + w + ", surface " + w.mWinAnimator.mSurface);
+ winAnimator.mDrawPending = true;
+ winAnimator.mCommitDrawPending = false;
w.mReadyToShow = false;
if (w.mAppToken != null) {
w.mAppToken.allDrawn = false;
@@ -8237,15 +8097,15 @@
}
if (!mResizingWindows.contains(w)) {
if (DEBUG_RESIZE || DEBUG_ORIENTATION) Slog.v(TAG,
- "Resizing window " + w + " to " + w.mSurfaceW
- + "x" + w.mSurfaceH);
+ "Resizing window " + w + " to " + w.mWinAnimator.mSurfaceW
+ + "x" + w.mWinAnimator.mSurfaceH);
mResizingWindows.add(w);
}
} else if (w.mOrientationChanging) {
if (w.isDrawnLw()) {
if (DEBUG_ORIENTATION) Slog.v(TAG,
"Orientation not waiting for draw in "
- + w + ", surface " + w.mSurface);
+ + w + ", surface " + w.mWinAnimator.mSurface);
w.mOrientationChanging = false;
}
}
@@ -8266,7 +8126,7 @@
final int attrFlags = attrs.flags;
final boolean canBeSeen = w.isDisplayedLw();
- if (w.mSurface != null) {
+ if (w.mWinAnimator.mSurface != null) {
if ((attrFlags&FLAG_KEEP_SCREEN_ON) != 0) {
mInnerFields.mHoldScreen = w.mSession;
}
@@ -8418,7 +8278,7 @@
mPolicy.beginAnimationLw(dw, dh);
for (i = mWindows.size() - 1; i >= 0; i--) {
WindowState w = mWindows.get(i);
- if (w.mSurface != null) {
+ if (w.mWinAnimator.mSurface != null) {
mPolicy.animatingWindowLw(w, w.mAttrs);
}
}
@@ -8495,9 +8355,33 @@
mPendingLayoutChanges |= animateAwayWallpaperLocked();
if (DEBUG_LAYOUT_REPEATS) debugLayoutRepeats("after animateAwayWallpaperLocked");
}
+
- mPendingLayoutChanges |= testWallpaperAndBackgroundLocked();
- if (DEBUG_LAYOUT_REPEATS) debugLayoutRepeats("after testWallpaperAndBackgroundLocked");
+ if (mInnerFields.mWallpaperMayChange) {
+ if (WindowManagerService.DEBUG_WALLPAPER) Slog.v(TAG,
+ "Wallpaper may change! Adjusting");
+ mInnerFields.mAdjResult |= adjustWallpaperWindowsLocked();
+ }
+
+ if ((mInnerFields.mAdjResult&ADJUST_WALLPAPER_LAYERS_CHANGED) != 0) {
+ if (DEBUG_WALLPAPER) Slog.v(TAG,
+ "Wallpaper layer changed: assigning layers + relayout");
+ mPendingLayoutChanges |= PhoneWindowManager.FINISH_LAYOUT_REDO_LAYOUT;
+ assignLayersLocked();
+ } else if ((mInnerFields.mAdjResult&ADJUST_WALLPAPER_VISIBILITY_CHANGED) != 0) {
+ if (DEBUG_WALLPAPER) Slog.v(TAG,
+ "Wallpaper visibility changed: relayout");
+ mPendingLayoutChanges |= PhoneWindowManager.FINISH_LAYOUT_REDO_LAYOUT;
+ }
+
+ if (mFocusMayChange) {
+ mFocusMayChange = false;
+ if (updateFocusedWindowLocked(UPDATE_FOCUS_PLACING_SURFACES,
+ false /*updateInputWindows*/)) {
+ mPendingLayoutChanges |= PhoneWindowManager.FINISH_LAYOUT_REDO_ANIM;
+ mInnerFields.mAdjResult = 0;
+ }
+ }
if (mLayoutNeeded) {
mPendingLayoutChanges |= PhoneWindowManager.FINISH_LAYOUT_REDO_LAYOUT;
@@ -8506,9 +8390,45 @@
final int N = mWindows.size();
for (i=N-1; i>=0; i--) {
- WindowState w = mWindows.get(i);
+ final WindowState w = mWindows.get(i);
+ final WindowStateAnimator winAnimator = w.mWinAnimator;
// TODO(cmautner): Can this move up to the loop at the end of try/catch above?
updateResizingWindows(w);
+
+ // Moved from updateWindowsAndWallpaperLocked().
+ if (winAnimator.mSurface != null) {
+ // Take care of the window being ready to display.
+ if (winAnimator.commitFinishDrawingLocked(currentTime)) {
+ if ((w.mAttrs.flags
+ & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
+ if (WindowManagerService.DEBUG_WALLPAPER) Slog.v(TAG,
+ "First draw done in potential wallpaper target " + w);
+ mInnerFields.mWallpaperMayChange = true;
+ mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
+ if (WindowManagerService.DEBUG_LAYOUT_REPEATS) {
+ debugLayoutRepeats("updateWindowsAndWallpaperLocked 1");
+ }
+ }
+ }
+
+ // If the window has moved due to its containing
+ // content frame changing, then we'd like to animate
+ // it. The checks here are ordered by what is least
+ // likely to be true first.
+ if (w.shouldAnimateMove()) {
+ // Frame has moved, containing content frame
+ // has also moved, and we're not currently animating...
+ // let's do something.
+ Animation a = AnimationUtils.loadAnimation(mContext,
+ com.android.internal.R.anim.window_move_from_decor);
+ winAnimator.setAnimation(a);
+ winAnimator.mAnimDw = w.mLastFrame.left - w.mFrame.left;
+ winAnimator.mAnimDh = w.mLastFrame.top - w.mFrame.top;
+ } else {
+ winAnimator.mAnimDw = innerDw;
+ winAnimator.mAnimDh = innerDh;
+ }
+ }
}
// Update animations of all applications, including those
@@ -8540,6 +8460,7 @@
do {
i--;
WindowState win = mResizingWindows.get(i);
+ final WindowStateAnimator winAnimator = win.mWinAnimator;
try {
if (DEBUG_RESIZE || DEBUG_ORIENTATION) Slog.v(TAG,
"Reporting new frame to " + win + ": " + win.mCompatFrame);
@@ -8551,19 +8472,20 @@
if ((DEBUG_RESIZE || DEBUG_ORIENTATION || DEBUG_CONFIGURATION)
&& configChanged) {
Slog.i(TAG, "Sending new config to window " + win + ": "
- + win.mSurfaceW + "x" + win.mSurfaceH
+ + winAnimator.mSurfaceW + "x" + winAnimator.mSurfaceH
+ " / " + mCurConfiguration + " / 0x"
+ Integer.toHexString(diff));
}
win.mConfiguration = mCurConfiguration;
- if (DEBUG_ORIENTATION && win.mDrawPending) Slog.i(
+ if (DEBUG_ORIENTATION && winAnimator.mDrawPending) Slog.i(
TAG, "Resizing " + win + " WITH DRAW PENDING");
- win.mClient.resized((int)win.mSurfaceW, (int)win.mSurfaceH,
- win.mLastContentInsets, win.mLastVisibleInsets, win.mDrawPending,
- configChanged ? win.mConfiguration : null);
+ win.mClient.resized((int)winAnimator.mSurfaceW,
+ (int)winAnimator.mSurfaceH,
+ win.mLastContentInsets, win.mLastVisibleInsets,
+ winAnimator.mDrawPending, configChanged ? win.mConfiguration : null);
win.mContentInsetsChanged = false;
win.mVisibleInsetsChanged = false;
- win.mSurfaceResized = false;
+ winAnimator.mSurfaceResized = false;
} catch (RemoteException e) {
win.mOrientationChanging = false;
}
@@ -8585,7 +8507,7 @@
if (win == mWallpaperTarget) {
wallpaperDestroyed = true;
}
- win.destroySurfaceLocked();
+ win.mWinAnimator.destroySurfaceLocked();
} while (i > 0);
mDestroySurface.clear();
}
@@ -8711,7 +8633,7 @@
}
mWaitingForDrawn.remove(pair);
mH.removeMessages(H.WAITING_FOR_DRAWN_TIMEOUT, pair);
- } else if (win.mSurfaceShown) {
+ } else if (win.mWinAnimator.mSurfaceShown) {
// Window is now drawn (and shown).
try {
pair.second.sendResult(null);
@@ -8763,48 +8685,19 @@
void scheduleAnimationLocked() {
if (!mAnimationScheduled) {
- mChoreographer.postAnimationCallback(mAnimationRunnable, null);
+ mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mAnimationRunnable, null);
mAnimationScheduled = true;
}
}
- /**
- * Have the surface flinger show a surface, robustly dealing with
- * error conditions. In particular, if there is not enough memory
- * to show the surface, then we will try to get rid of other surfaces
- * in order to succeed.
- *
- * @return Returns true if the surface was successfully shown.
- */
- boolean showSurfaceRobustlyLocked(WindowState win) {
- try {
- if (win.mSurface != null) {
- win.mSurfaceShown = true;
- win.mSurface.show();
- if (win.mTurnOnScreen) {
- if (DEBUG_VISIBILITY) Slog.v(TAG,
- "Show surface turning screen on: " + win);
- win.mTurnOnScreen = false;
- mTurnOnScreen = true;
- }
- }
- return true;
- } catch (RuntimeException e) {
- Slog.w(TAG, "Failure showing surface " + win.mSurface + " in " + win, e);
- }
-
- reclaimSomeSurfaceMemoryLocked(win, "show", true);
-
- return false;
- }
-
- boolean reclaimSomeSurfaceMemoryLocked(WindowState win, String operation, boolean secure) {
- final Surface surface = win.mSurface;
+ boolean reclaimSomeSurfaceMemoryLocked(WindowStateAnimator winAnimator, String operation,
+ boolean secure) {
+ final Surface surface = winAnimator.mSurface;
boolean leakedSurface = false;
boolean killedApps = false;
- EventLog.writeEvent(EventLogTags.WM_NO_SURFACE_MEMORY, win.toString(),
- win.mSession.mPid, operation);
+ EventLog.writeEvent(EventLogTags.WM_NO_SURFACE_MEMORY, winAnimator.mWin.toString(),
+ winAnimator.mSession.mPid, operation);
if (mForceRemoves == null) {
mForceRemoves = new ArrayList<WindowState>();
@@ -8819,29 +8712,30 @@
Slog.i(TAG, "Out of memory for surface! Looking for leaks...");
for (int i=0; i<N; i++) {
WindowState ws = mWindows.get(i);
- if (ws.mSurface != null) {
- if (!mSessions.contains(ws.mSession)) {
+ WindowStateAnimator wsa = ws.mWinAnimator;
+ if (wsa.mSurface != null) {
+ if (!mSessions.contains(wsa.mSession)) {
Slog.w(TAG, "LEAKED SURFACE (session doesn't exist): "
- + ws + " surface=" + ws.mSurface
- + " token=" + win.mToken
+ + ws + " surface=" + wsa.mSurface
+ + " token=" + ws.mToken
+ " pid=" + ws.mSession.mPid
+ " uid=" + ws.mSession.mUid);
if (SHOW_TRANSACTIONS) logSurface(ws, "LEAK DESTROY", null);
- ws.mSurface.destroy();
- ws.mSurfaceShown = false;
- ws.mSurface = null;
+ wsa.mSurface.destroy();
+ wsa.mSurfaceShown = false;
+ wsa.mSurface = null;
mForceRemoves.add(ws);
i--;
N--;
leakedSurface = true;
} else if (ws.mAppToken != null && ws.mAppToken.clientHidden) {
Slog.w(TAG, "LEAKED SURFACE (app token hidden): "
- + ws + " surface=" + ws.mSurface
- + " token=" + win.mAppToken);
+ + ws + " surface=" + wsa.mSurface
+ + " token=" + ws.mAppToken);
if (SHOW_TRANSACTIONS) logSurface(ws, "LEAK DESTROY", null);
- ws.mSurface.destroy();
- ws.mSurfaceShown = false;
- ws.mSurface = null;
+ wsa.mSurface.destroy();
+ wsa.mSurfaceShown = false;
+ wsa.mSurface = null;
leakedSurface = true;
}
}
@@ -8851,9 +8745,9 @@
Slog.w(TAG, "No leaked surfaces; killing applicatons!");
SparseIntArray pidCandidates = new SparseIntArray();
for (int i=0; i<N; i++) {
- WindowState ws = mWindows.get(i);
- if (ws.mSurface != null) {
- pidCandidates.append(ws.mSession.mPid, ws.mSession.mPid);
+ WindowStateAnimator wsa = mWindows.get(i).mWinAnimator;
+ if (wsa.mSurface != null) {
+ pidCandidates.append(wsa.mSession.mPid, wsa.mSession.mPid);
}
}
if (pidCandidates.size() > 0) {
@@ -8875,15 +8769,15 @@
// surface and ask the app to request another one.
Slog.w(TAG, "Looks like we have reclaimed some memory, clearing surface for retry.");
if (surface != null) {
- if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) logSurface(win,
+ if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) logSurface(winAnimator.mWin,
"RECOVER DESTROY", null);
surface.destroy();
- win.mSurfaceShown = false;
- win.mSurface = null;
+ winAnimator.mSurfaceShown = false;
+ winAnimator.mSurface = null;
}
try {
- win.mClient.dispatchGetNewSurface();
+ winAnimator.mWin.mClient.dispatchGetNewSurface();
} catch (RemoteException e) {
}
}
@@ -9051,11 +8945,11 @@
mAnimator.mScreenRotationAnimation.kill();
mAnimator.mScreenRotationAnimation = null;
}
- if (mAnimator.mScreenRotationAnimation == null) {
- mAnimator.mScreenRotationAnimation = new ScreenRotationAnimation(mContext,
- mFxSession, inTransaction, mCurDisplayWidth, mCurDisplayHeight,
- mDisplay.getRotation());
- }
+
+ mAnimator.mScreenRotationAnimation = new ScreenRotationAnimation(mContext,
+ mFxSession, inTransaction, mCurDisplayWidth, mCurDisplayHeight,
+ mDisplay.getRotation());
+
if (!mAnimator.mScreenRotationAnimation.hasScreenshot()) {
Surface.freezeDisplay(0);
}
@@ -9070,6 +8964,10 @@
}
if (mWaitingForConfig || mAppsFreezingScreen > 0 || mWindowsFreezingScreen) {
+ if (DEBUG_ORIENTATION) Slog.d(TAG,
+ "stopFreezingDisplayLocked: Returning mWaitingForConfig=" + mWaitingForConfig
+ + ", mAppsFreezingScreen=" + mAppsFreezingScreen
+ + ", mWindowsFreezingScreen=" + mWindowsFreezingScreen);
return;
}
@@ -9543,9 +9441,6 @@
pw.print(" mLowerWallpaperTarget="); pw.println(mLowerWallpaperTarget);
pw.print(" mUpperWallpaperTarget="); pw.println(mUpperWallpaperTarget);
}
- if (mWindowDetachedWallpaper != null) {
- pw.print(" mWindowDetachedWallpaper="); pw.println(mWindowDetachedWallpaper);
- }
pw.print(" mLastWallpaperX="); pw.print(mLastWallpaperX);
pw.print(" mLastWallpaperY="); pw.println(mLastWallpaperY);
if (mInputMethodAnimLayerAdjustment != 0 ||
@@ -9555,10 +9450,6 @@
pw.print(" mWallpaperAnimLayerAdjustment=");
pw.println(mWallpaperAnimLayerAdjustment);
}
- if (mWindowAnimationBackgroundSurface != null) {
- pw.println(" mWindowAnimationBackgroundSurface:");
- mWindowAnimationBackgroundSurface.printTo(" ", pw);
- }
pw.print(" mSystemBooted="); pw.print(mSystemBooted);
pw.print(" mDisplayEnabled="); pw.println(mDisplayEnabled);
pw.print(" mLayoutNeeded="); pw.println(mLayoutNeeded);
@@ -9616,7 +9507,7 @@
synchronized(mWindowMap) {
for (int i=mWindows.size()-1; i>=0; i--) {
WindowState w = mWindows.get(i);
- if (w.mSurfaceShown) {
+ if (w.mWinAnimator.mSurfaceShown) {
windows.add(w);
}
}
@@ -9772,10 +9663,19 @@
public void onHardKeyboardStatusChange(boolean available, boolean enabled);
}
+ void notifyAnimationChangedLayout(final int pendingLayoutChanges) {
+ mPendingLayoutChanges |= pendingLayoutChanges;
+ requestTraversalLocked();
+ }
+
void debugLayoutRepeats(final String msg) {
if (mLayoutRepeatCount >= LAYOUT_REPEAT_THRESHOLD) {
Slog.v(TAG, "Layouts looping: " + msg);
Slog.v(TAG, "mPendingLayoutChanges = 0x" + Integer.toHexString(mPendingLayoutChanges));
}
}
+
+ void bulkSetParameters(final int bulkUpdateParams) {
+ mH.sendMessage(mH.obtainMessage(H.BULK_UPDATE_PARAMETERS, bulkUpdateParams, 0));
+ }
}
diff --git a/services/java/com/android/server/wm/WindowState.java b/services/java/com/android/server/wm/WindowState.java
index 615cd80..7f63429 100644
--- a/services/java/com/android/server/wm/WindowState.java
+++ b/services/java/com/android/server/wm/WindowState.java
@@ -19,13 +19,11 @@
import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW;
import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
-import com.android.server.wm.WindowManagerService.H;
-
+import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
@@ -39,13 +37,10 @@
import android.view.IApplicationToken;
import android.view.IWindow;
import android.view.InputChannel;
-import android.view.Surface;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.WindowManagerPolicy;
-import android.view.WindowManager.LayoutParams;
-import android.view.animation.Transformation;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -54,12 +49,16 @@
* A window in the window manager.
*/
final class WindowState implements WindowManagerPolicy.WindowState {
+ static final String TAG = "WindowState";
+
static final boolean DEBUG_VISIBILITY = WindowManagerService.DEBUG_VISIBILITY;
static final boolean SHOW_TRANSACTIONS = WindowManagerService.SHOW_TRANSACTIONS;
static final boolean SHOW_LIGHT_TRANSACTIONS = WindowManagerService.SHOW_LIGHT_TRANSACTIONS;
static final boolean SHOW_SURFACE_ALLOC = WindowManagerService.SHOW_SURFACE_ALLOC;
final WindowManagerService mService;
+ final WindowManagerPolicy mPolicy;
+ final Context mContext;
final Session mSession;
final IWindow mClient;
WindowToken mToken;
@@ -83,10 +82,6 @@
boolean mPolicyVisibility = true;
boolean mPolicyVisibilityAfterAnim = true;
boolean mAppFreezing;
- Surface mSurface;
- Surface mPendingDestroySurface;
- boolean mReportDestroySurface;
- boolean mSurfacePendingDestroy;
boolean mAttachedHidden; // is our parent window hidden?
boolean mLastHidden; // was this window last hidden?
boolean mWallpaperVisible; // for wallpaper, what was last vis report?
@@ -101,8 +96,6 @@
int mLastRequestedHeight;
int mLayer;
- int mAnimLayer;
- int mLastLayer;
boolean mHaveFrame;
boolean mObscured;
boolean mTurnOnScreen;
@@ -119,18 +112,6 @@
final RectF mShownFrame = new RectF();
/**
- * Set when we have changed the size of the surface, to know that
- * we must tell them application to resize (and thus redraw itself).
- */
- boolean mSurfaceResized;
-
- /**
- * Set if the client has asked that the destroy of its surface be delayed
- * until it explicitly says it is okay.
- */
- boolean mSurfaceDestroyDeferred;
-
- /**
* Insets that determine the actually visible area. These are in the application's
* coordinate space (without compatibility scale applied).
*/
@@ -178,11 +159,8 @@
int mTouchableInsets = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
// Current transformation being applied.
- boolean mHaveMatrix;
float mGlobalScale=1;
float mInvGlobalScale=1;
- float mDsDx=1, mDtDx=0, mDsDy=0, mDtDy=1;
- float mLastDsDx=1, mLastDtDx=0, mLastDsDy=0, mLastDtDy=1;
float mHScale=1, mVScale=1;
float mLastHScale=1, mLastVScale=1;
final Matrix mTmpMatrix = new Matrix();
@@ -202,14 +180,6 @@
boolean mContentChanged;
- float mShownAlpha = 1;
- float mAlpha = 1;
- float mLastAlpha = 1;
-
- // Set to true if, when the window gets displayed, it should perform
- // an enter animation.
- boolean mEnterAnimationPending;
-
// If a window showing a wallpaper: the requested offset for the
// wallpaper; if a wallpaper window: the currently applied offset.
float mWallpaperX = -1;
@@ -236,15 +206,6 @@
// when in that case until the layout is done.
boolean mLayoutNeeded;
- // This is set after the Surface has been created but before the
- // window has been drawn. During this time the surface is hidden.
- boolean mDrawPending;
-
- // This is set after the window has finished drawing for the first
- // time but before its surface is shown. The surface will be
- // displayed when the next layout is run.
- boolean mCommitDrawPending;
-
// This is set during the time after the window's drawing has been
// committed, and before its surface is actually shown. It is used
// to delay showing the surface until all windows in a token are ready
@@ -274,12 +235,6 @@
// rebuilding window list.
boolean mRebuilding;
- // For debugging, this is the last information given to the surface flinger.
- boolean mSurfaceShown;
- float mSurfaceX, mSurfaceY, mSurfaceW, mSurfaceH;
- int mSurfaceLayer;
- float mSurfaceAlpha;
-
// Input channel and input window handle used by the input dispatcher.
final InputWindowHandle mInputWindowHandle;
InputChannel mInputChannel;
@@ -289,11 +244,6 @@
CharSequence mLastTitle;
boolean mWasPaused;
- // Used to save animation distances between the time they are calculated and when they are
- // used.
- int mAnimDw;
- int mAnimDh;
-
final WindowStateAnimator mWinAnimator;
WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
@@ -305,12 +255,13 @@
mToken = token;
mAttrs.copyFrom(a);
mViewVisibility = viewVisibility;
+ mPolicy = mService.mPolicy;
+ mContext = mService.mContext;
DeathRecipient deathRecipient = new DeathRecipient();
- mAlpha = a.alpha;
mSeq = seq;
mEnforceSizeCompat = (mAttrs.flags & FLAG_COMPATIBLE_WINDOW) != 0;
if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "Window " + this + " client=" + c.asBinder()
+ TAG, "Window " + this + " client=" + c.asBinder()
+ " token=" + token + " (" + mAttrs.token + ")");
try {
c.asBinder().linkToDeath(deathRecipient, 0);
@@ -333,12 +284,12 @@
mAttrs.type <= LAST_SUB_WINDOW)) {
// The multiplier here is to reserve space for multiple
// windows in the same type layer.
- mBaseLayer = mService.mPolicy.windowTypeToLayerLw(
+ mBaseLayer = mPolicy.windowTypeToLayerLw(
attachedWindow.mAttrs.type) * WindowManagerService.TYPE_LAYER_MULTIPLIER
+ WindowManagerService.TYPE_LAYER_OFFSET;
- mSubLayer = mService.mPolicy.subWindowTypeToLayerLw(a.type);
+ mSubLayer = mPolicy.subWindowTypeToLayerLw(a.type);
mAttachedWindow = attachedWindow;
- if (WindowManagerService.DEBUG_ADD_REMOVE) Slog.v(WindowManagerService.TAG, "Adding " + this + " to " + mAttachedWindow);
+ if (WindowManagerService.DEBUG_ADD_REMOVE) Slog.v(TAG, "Adding " + this + " to " + mAttachedWindow);
mAttachedWindow.mChildWindows.add(this);
mLayoutAttached = mAttrs.type !=
WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
@@ -349,7 +300,7 @@
} else {
// The multiplier here is to reserve space for multiple
// windows in the same type layer.
- mBaseLayer = mService.mPolicy.windowTypeToLayerLw(a.type)
+ mBaseLayer = mPolicy.windowTypeToLayerLw(a.type)
* WindowManagerService.TYPE_LAYER_MULTIPLIER
+ WindowManagerService.TYPE_LAYER_OFFSET;
mSubLayer = 0;
@@ -362,6 +313,7 @@
}
mWinAnimator = new WindowStateAnimator(service, this, mAttachedWindow);
+ mWinAnimator.mAlpha = a.alpha;
WindowState appWin = this;
while (appWin.mAttachedWindow != null) {
@@ -378,7 +330,6 @@
mRootToken = appToken;
mAppToken = appToken.appWindowToken;
- mSurface = null;
mRequestedWidth = 0;
mRequestedHeight = 0;
mLastRequestedWidth = 0;
@@ -386,15 +337,13 @@
mXOffset = 0;
mYOffset = 0;
mLayer = 0;
- mAnimLayer = 0;
- mLastLayer = 0;
mInputWindowHandle = new InputWindowHandle(
mAppToken != null ? mAppToken.mInputApplicationHandle : null, this);
}
void attach() {
if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "Attaching " + this + " token=" + mToken
+ TAG, "Attaching " + this + " token=" + mToken
+ ", list=" + mToken.windows);
mSession.windowAddedLocked();
}
@@ -532,7 +481,7 @@
if (WindowManagerService.localLOGV) {
//if ("com.google.android.youtube".equals(mAttrs.packageName)
// && mAttrs.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
- Slog.v(WindowManagerService.TAG, "Resolving (mRequestedWidth="
+ Slog.v(TAG, "Resolving (mRequestedWidth="
+ mRequestedWidth + ", mRequestedheight="
+ mRequestedHeight + ") to" + " (pw=" + pw + ", ph=" + ph
+ "): frame=" + mFrame.toShortString()
@@ -636,334 +585,6 @@
return mAppToken != null ? mAppToken.firstWindowDrawn : false;
}
- Surface createSurfaceLocked() {
- if (mSurface == null) {
- mReportDestroySurface = false;
- mSurfacePendingDestroy = false;
- if (WindowManagerService.DEBUG_ORIENTATION) Slog.i(WindowManagerService.TAG,
- "createSurface " + this + ": DRAW NOW PENDING");
- mDrawPending = true;
- mCommitDrawPending = false;
- mReadyToShow = false;
- if (mAppToken != null) {
- mAppToken.allDrawn = false;
- }
-
- mService.makeWindowFreezingScreenIfNeededLocked(this);
-
- int flags = 0;
-
- if ((mAttrs.flags&WindowManager.LayoutParams.FLAG_SECURE) != 0) {
- flags |= Surface.SECURE;
- }
- if (DEBUG_VISIBILITY) Slog.v(
- WindowManagerService.TAG, "Creating surface in session "
- + mSession.mSurfaceSession + " window " + this
- + " w=" + mCompatFrame.width()
- + " h=" + mCompatFrame.height() + " format="
- + mAttrs.format + " flags=" + flags);
-
- int w = mCompatFrame.width();
- int h = mCompatFrame.height();
- if ((mAttrs.flags & LayoutParams.FLAG_SCALED) != 0) {
- // for a scaled surface, we always want the requested
- // size.
- w = mRequestedWidth;
- h = mRequestedHeight;
- }
-
- // Something is wrong and SurfaceFlinger will not like this,
- // try to revert to sane values
- if (w <= 0) w = 1;
- if (h <= 0) h = 1;
-
- mSurfaceShown = false;
- mSurfaceLayer = 0;
- mSurfaceAlpha = 1;
- mSurfaceX = 0;
- mSurfaceY = 0;
- mSurfaceW = w;
- mSurfaceH = h;
- try {
- final boolean isHwAccelerated = (mAttrs.flags &
- WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
- final int format = isHwAccelerated ? PixelFormat.TRANSLUCENT : mAttrs.format;
- if (!PixelFormat.formatHasAlpha(mAttrs.format)) {
- flags |= Surface.OPAQUE;
- }
- mSurface = new Surface(
- mSession.mSurfaceSession, mSession.mPid,
- mAttrs.getTitle().toString(),
- 0, w, h, format, flags);
- if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) Slog.i(WindowManagerService.TAG,
- " CREATE SURFACE "
- + mSurface + " IN SESSION "
- + mSession.mSurfaceSession
- + ": pid=" + mSession.mPid + " format="
- + mAttrs.format + " flags=0x"
- + Integer.toHexString(flags)
- + " / " + this);
- } catch (Surface.OutOfResourcesException e) {
- Slog.w(WindowManagerService.TAG, "OutOfResourcesException creating surface");
- mService.reclaimSomeSurfaceMemoryLocked(this, "create", true);
- return null;
- } catch (Exception e) {
- Slog.e(WindowManagerService.TAG, "Exception creating surface", e);
- return null;
- }
-
- if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "Got surface: " + mSurface
- + ", set left=" + mFrame.left + " top=" + mFrame.top
- + ", animLayer=" + mAnimLayer);
- if (SHOW_LIGHT_TRANSACTIONS) {
- Slog.i(WindowManagerService.TAG, ">>> OPEN TRANSACTION createSurfaceLocked");
- WindowManagerService.logSurface(this, "CREATE pos=(" + mFrame.left
- + "," + mFrame.top + ") (" +
- mCompatFrame.width() + "x" + mCompatFrame.height() + "), layer=" +
- mAnimLayer + " HIDE", null);
- }
- Surface.openTransaction();
- try {
- try {
- mSurfaceX = mFrame.left + mXOffset;
- mSurfaceY = mFrame.top + mYOffset;
- mSurface.setPosition(mSurfaceX, mSurfaceY);
- mSurfaceLayer = mAnimLayer;
- mSurface.setLayer(mAnimLayer);
- mSurfaceShown = false;
- mSurface.hide();
- if ((mAttrs.flags&WindowManager.LayoutParams.FLAG_DITHER) != 0) {
- if (SHOW_TRANSACTIONS) WindowManagerService.logSurface(this, "DITHER", null);
- mSurface.setFlags(Surface.SURFACE_DITHER,
- Surface.SURFACE_DITHER);
- }
- } catch (RuntimeException e) {
- Slog.w(WindowManagerService.TAG, "Error creating surface in " + w, e);
- mService.reclaimSomeSurfaceMemoryLocked(this, "create-init", true);
- }
- mLastHidden = true;
- } finally {
- Surface.closeTransaction();
- if (SHOW_LIGHT_TRANSACTIONS) Slog.i(WindowManagerService.TAG,
- "<<< CLOSE TRANSACTION createSurfaceLocked");
- }
- if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "Created surface " + this);
- }
- return mSurface;
- }
-
- void destroySurfaceLocked() {
- if (mAppToken != null && this == mAppToken.startingWindow) {
- mAppToken.startingDisplayed = false;
- }
-
- if (mSurface != null) {
- mDrawPending = false;
- mCommitDrawPending = false;
- mReadyToShow = false;
-
- int i = mChildWindows.size();
- while (i > 0) {
- i--;
- WindowState c = mChildWindows.get(i);
- c.mAttachedHidden = true;
- }
-
- if (mReportDestroySurface) {
- mReportDestroySurface = false;
- mSurfacePendingDestroy = true;
- try {
- mClient.dispatchGetNewSurface();
- // We'll really destroy on the next time around.
- return;
- } catch (RemoteException e) {
- }
- }
-
- try {
- if (DEBUG_VISIBILITY) {
- RuntimeException e = null;
- if (!WindowManagerService.HIDE_STACK_CRAWLS) {
- e = new RuntimeException();
- e.fillInStackTrace();
- }
- Slog.w(WindowManagerService.TAG, "Window " + this + " destroying surface "
- + mSurface + ", session " + mSession, e);
- }
- if (mSurfaceDestroyDeferred) {
- if (mSurface != null && mPendingDestroySurface != mSurface) {
- if (mPendingDestroySurface != null) {
- if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) {
- RuntimeException e = null;
- if (!WindowManagerService.HIDE_STACK_CRAWLS) {
- e = new RuntimeException();
- e.fillInStackTrace();
- }
- WindowManagerService.logSurface(this, "DESTROY PENDING", e);
- }
- mPendingDestroySurface.destroy();
- }
- mPendingDestroySurface = mSurface;
- }
- } else {
- if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) {
- RuntimeException e = null;
- if (!WindowManagerService.HIDE_STACK_CRAWLS) {
- e = new RuntimeException();
- e.fillInStackTrace();
- }
- WindowManagerService.logSurface(this, "DESTROY", e);
- }
- mSurface.destroy();
- }
- } catch (RuntimeException e) {
- Slog.w(WindowManagerService.TAG, "Exception thrown when destroying Window " + this
- + " surface " + mSurface + " session " + mSession
- + ": " + e.toString());
- }
-
- mSurfaceShown = false;
- mSurface = null;
- }
- }
-
- void destroyDeferredSurfaceLocked() {
- try {
- if (mPendingDestroySurface != null) {
- if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) {
- RuntimeException e = null;
- if (!WindowManagerService.HIDE_STACK_CRAWLS) {
- e = new RuntimeException();
- e.fillInStackTrace();
- }
- mService.logSurface(this, "DESTROY PENDING", e);
- }
- mPendingDestroySurface.destroy();
- }
- } catch (RuntimeException e) {
- Slog.w(WindowManagerService.TAG, "Exception thrown when destroying Window "
- + this + " surface " + mPendingDestroySurface
- + " session " + mSession + ": " + e.toString());
- }
- mSurfaceDestroyDeferred = false;
- mPendingDestroySurface = null;
- }
-
- boolean finishDrawingLocked() {
- if (mDrawPending) {
- if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION) Slog.v(
- WindowManagerService.TAG, "finishDrawingLocked: " + this + " in " + mSurface);
- mCommitDrawPending = true;
- mDrawPending = false;
- return true;
- }
- return false;
- }
-
- // This must be called while inside a transaction.
- boolean commitFinishDrawingLocked(long currentTime) {
- //Slog.i(TAG, "commitFinishDrawingLocked: " + mSurface);
- if (!mCommitDrawPending) {
- return false;
- }
- mCommitDrawPending = false;
- mReadyToShow = true;
- final boolean starting = mAttrs.type == TYPE_APPLICATION_STARTING;
- final AppWindowToken atoken = mAppToken;
- if (atoken == null || atoken.allDrawn || starting) {
- performShowLocked();
- }
- return true;
- }
-
- // This must be called while inside a transaction.
- boolean performShowLocked() {
- if (DEBUG_VISIBILITY) {
- RuntimeException e = null;
- if (!WindowManagerService.HIDE_STACK_CRAWLS) {
- e = new RuntimeException();
- e.fillInStackTrace();
- }
- Slog.v(WindowManagerService.TAG, "performShow on " + this
- + ": readyToShow=" + mReadyToShow + " readyForDisplay=" + isReadyForDisplay()
- + " starting=" + (mAttrs.type == TYPE_APPLICATION_STARTING), e);
- }
- if (mReadyToShow && isReadyForDisplay()) {
- if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION) WindowManagerService.logSurface(this,
- "SHOW (performShowLocked)", null);
- if (DEBUG_VISIBILITY) Slog.v(WindowManagerService.TAG, "Showing " + this
- + " during animation: policyVis=" + mPolicyVisibility
- + " attHidden=" + mAttachedHidden
- + " tok.hiddenRequested="
- + (mAppToken != null ? mAppToken.hiddenRequested : false)
- + " tok.hidden="
- + (mAppToken != null ? mAppToken.hidden : false)
- + " animating=" + mWinAnimator.mAnimating
- + " tok animating="
- + (mAppToken != null ? mAppToken.animating : false));
- if (!mService.showSurfaceRobustlyLocked(this)) {
- return false;
- }
-
- mService.enableScreenIfNeededLocked();
-
- mService.applyEnterAnimationLocked(this);
-
- mLastAlpha = -1;
- mHasDrawn = true;
- mLastHidden = false;
- mReadyToShow = false;
-
- int i = mChildWindows.size();
- while (i > 0) {
- i--;
- WindowState c = mChildWindows.get(i);
- if (c.mAttachedHidden) {
- c.mAttachedHidden = false;
- if (c.mSurface != null) {
- c.performShowLocked();
- // It hadn't been shown, which means layout not
- // performed on it, so now we want to make sure to
- // do a layout. If called from within the transaction
- // loop, this will cause it to restart with a new
- // layout.
- mService.mLayoutNeeded = true;
- }
- }
- }
-
- if (mAttrs.type != TYPE_APPLICATION_STARTING
- && mAppToken != null) {
- mAppToken.firstWindowDrawn = true;
-
- if (mAppToken.startingData != null) {
- if (WindowManagerService.DEBUG_STARTING_WINDOW ||
- WindowManagerService.DEBUG_ANIM) Slog.v(WindowManagerService.TAG,
- "Finish starting " + mToken
- + ": first real window is shown, no animation");
- // If this initial window is animating, stop it -- we
- // will do an animation to reveal it from behind the
- // starting window, so there is no need for it to also
- // be doing its own stuff.
- if (mWinAnimator.mAnimation != null) {
- mWinAnimator.mAnimation.cancel();
- mWinAnimator.mAnimation = null;
- // Make sure we clean up the animation.
- mWinAnimator.mAnimating = true;
- }
- mService.mFinishedStarting.add(mAppToken);
- mService.mH.sendEmptyMessage(H.FINISHED_STARTING);
- }
- mAppToken.updateReportedVisibilityLocked();
- }
- } else {
- return false;
- }
- return true;
- }
-
boolean isIdentityMatrix(float dsdx, float dtdx, float dsdy, float dtdy) {
if (dsdx < .99999f || dsdx > 1.00001f) return false;
if (dtdy < .99999f || dtdy > 1.00001f) return false;
@@ -981,150 +602,6 @@
}
}
- void computeShownFrameLocked() {
- final boolean selfTransformation = mWinAnimator.mHasLocalTransformation;
- Transformation attachedTransformation =
- (mAttachedWindow != null && mAttachedWindow.mWinAnimator.mHasLocalTransformation)
- ? mAttachedWindow.mWinAnimator.mTransformation : null;
- Transformation appTransformation =
- (mAppToken != null && mAppToken.hasTransformation)
- ? mAppToken.transformation : null;
-
- // Wallpapers are animated based on the "real" window they
- // are currently targeting.
- if (mAttrs.type == TYPE_WALLPAPER && mService.mLowerWallpaperTarget == null
- && mService.mWallpaperTarget != null) {
- if (mService.mWallpaperTarget.mWinAnimator.mHasLocalTransformation &&
- mService.mWallpaperTarget.mWinAnimator.mAnimation != null &&
- !mService.mWallpaperTarget.mWinAnimator.mAnimation.getDetachWallpaper()) {
- attachedTransformation = mService.mWallpaperTarget.mWinAnimator.mTransformation;
- if (WindowManagerService.DEBUG_WALLPAPER && attachedTransformation != null) {
- Slog.v(WindowManagerService.TAG, "WP target attached xform: " + attachedTransformation);
- }
- }
- if (mService.mWallpaperTarget.mAppToken != null &&
- mService.mWallpaperTarget.mAppToken.hasTransformation &&
- mService.mWallpaperTarget.mAppToken.animation != null &&
- !mService.mWallpaperTarget.mAppToken.animation.getDetachWallpaper()) {
- appTransformation = mService.mWallpaperTarget.mAppToken.transformation;
- if (WindowManagerService.DEBUG_WALLPAPER && appTransformation != null) {
- Slog.v(WindowManagerService.TAG, "WP target app xform: " + appTransformation);
- }
- }
- }
-
- final boolean screenAnimation = mService.mAnimator.mScreenRotationAnimation != null
- && mService.mAnimator.mScreenRotationAnimation.isAnimating();
- if (selfTransformation || attachedTransformation != null
- || appTransformation != null || screenAnimation) {
- // cache often used attributes locally
- final Rect frame = mFrame;
- final float tmpFloats[] = mService.mTmpFloats;
- final Matrix tmpMatrix = mTmpMatrix;
-
- // Compute the desired transformation.
- if (screenAnimation) {
- // If we are doing a screen animation, the global rotation
- // applied to windows can result in windows that are carefully
- // aligned with each other to slightly separate, allowing you
- // to see what is behind them. An unsightly mess. This...
- // thing... magically makes it call good: scale each window
- // slightly (two pixels larger in each dimension, from the
- // window's center).
- final float w = frame.width();
- final float h = frame.height();
- if (w>=1 && h>=1) {
- tmpMatrix.setScale(1 + 2/w, 1 + 2/h, w/2, h/2);
- } else {
- tmpMatrix.reset();
- }
- } else {
- tmpMatrix.reset();
- }
- tmpMatrix.postScale(mGlobalScale, mGlobalScale);
- if (selfTransformation) {
- tmpMatrix.postConcat(mWinAnimator.mTransformation.getMatrix());
- }
- tmpMatrix.postTranslate(frame.left + mXOffset, frame.top + mYOffset);
- if (attachedTransformation != null) {
- tmpMatrix.postConcat(attachedTransformation.getMatrix());
- }
- if (appTransformation != null) {
- tmpMatrix.postConcat(appTransformation.getMatrix());
- }
- if (screenAnimation) {
- tmpMatrix.postConcat(
- mService.mAnimator.mScreenRotationAnimation.getEnterTransformation().getMatrix());
- }
-
- // "convert" it into SurfaceFlinger's format
- // (a 2x2 matrix + an offset)
- // Here we must not transform the position of the surface
- // since it is already included in the transformation.
- //Slog.i(TAG, "Transform: " + matrix);
-
- mHaveMatrix = true;
- tmpMatrix.getValues(tmpFloats);
- mDsDx = tmpFloats[Matrix.MSCALE_X];
- mDtDx = tmpFloats[Matrix.MSKEW_Y];
- mDsDy = tmpFloats[Matrix.MSKEW_X];
- mDtDy = tmpFloats[Matrix.MSCALE_Y];
- float x = tmpFloats[Matrix.MTRANS_X];
- float y = tmpFloats[Matrix.MTRANS_Y];
- int w = frame.width();
- int h = frame.height();
- mShownFrame.set(x, y, x+w, y+h);
-
- // Now set the alpha... but because our current hardware
- // can't do alpha transformation on a non-opaque surface,
- // turn it off if we are running an animation that is also
- // transforming since it is more important to have that
- // animation be smooth.
- mShownAlpha = mAlpha;
- if (!mService.mLimitedAlphaCompositing
- || (!PixelFormat.formatHasAlpha(mAttrs.format)
- || (isIdentityMatrix(mDsDx, mDtDx, mDsDy, mDtDy)
- && x == frame.left && y == frame.top))) {
- //Slog.i(TAG, "Applying alpha transform");
- if (selfTransformation) {
- mShownAlpha *= mWinAnimator.mTransformation.getAlpha();
- }
- if (attachedTransformation != null) {
- mShownAlpha *= attachedTransformation.getAlpha();
- }
- if (appTransformation != null) {
- mShownAlpha *= appTransformation.getAlpha();
- }
- if (screenAnimation) {
- mShownAlpha *=
- mService.mAnimator.mScreenRotationAnimation.getEnterTransformation().getAlpha();
- }
- } else {
- //Slog.i(TAG, "Not applying alpha transform");
- }
-
- if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "computeShownFrameLocked: Animating " + this +
- ": " + mShownFrame +
- ", alpha=" + mWinAnimator.mTransformation.getAlpha() + ", mShownAlpha=" + mShownAlpha);
- return;
- }
-
- if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "computeShownFrameLocked: " + this +
- " not attached, mAlpha=" + mAlpha);
- mShownFrame.set(mFrame);
- if (mXOffset != 0 || mYOffset != 0) {
- mShownFrame.offset(mXOffset, mYOffset);
- }
- mShownAlpha = mAlpha;
- mHaveMatrix = false;
- mDsDx = mGlobalScale;
- mDtDx = 0;
- mDsDy = 0;
- mDtDy = mGlobalScale;
- }
-
/**
* Is this window visible? It is not visible if there is no
* surface, or we are in the process of running an exit animation
@@ -1132,7 +609,7 @@
*/
public boolean isVisibleLw() {
final AppWindowToken atoken = mAppToken;
- return mSurface != null && mPolicyVisibility && !mAttachedHidden
+ return mWinAnimator.mSurface != null && mPolicyVisibility && !mAttachedHidden
&& (atoken == null || !atoken.hiddenRequested)
&& !mExiting && !mDestroying;
}
@@ -1153,7 +630,7 @@
final AppWindowToken atoken = mAppToken;
final boolean animating = atoken != null
? (atoken.animation != null) : false;
- return mSurface != null && !mDestroying && !mExiting
+ return mWinAnimator.mSurface != null && !mDestroying && !mExiting
&& (atoken == null ? mPolicyVisibility : !atoken.hiddenRequested)
&& ((!mAttachedHidden && mViewVisibility == View.VISIBLE
&& !mRootToken.hidden)
@@ -1167,7 +644,7 @@
*/
public boolean isWinVisibleLw() {
final AppWindowToken atoken = mAppToken;
- return mSurface != null && mPolicyVisibility && !mAttachedHidden
+ return mWinAnimator.mSurface != null && mPolicyVisibility && !mAttachedHidden
&& (atoken == null || !atoken.hiddenRequested || atoken.animating)
&& !mExiting && !mDestroying;
}
@@ -1177,7 +654,7 @@
* the associated app token, not the pending requested hidden state.
*/
boolean isVisibleNow() {
- return mSurface != null && mPolicyVisibility && !mAttachedHidden
+ return mWinAnimator.mSurface != null && mPolicyVisibility && !mAttachedHidden
&& !mRootToken.hidden && !mExiting && !mDestroying;
}
@@ -1197,7 +674,7 @@
*/
boolean isVisibleOrAdding() {
final AppWindowToken atoken = mAppToken;
- return ((mSurface != null && !mReportDestroySurface)
+ return ((mWinAnimator.mSurface != null && !mWinAnimator.mReportDestroySurface)
|| (!mRelayoutCalled && mViewVisibility == View.VISIBLE))
&& mPolicyVisibility && !mAttachedHidden
&& (atoken == null || !atoken.hiddenRequested)
@@ -1212,11 +689,11 @@
boolean isOnScreen() {
final AppWindowToken atoken = mAppToken;
if (atoken != null) {
- return mSurface != null && mPolicyVisibility && !mDestroying
+ return mWinAnimator.mSurface != null && mPolicyVisibility && !mDestroying
&& ((!mAttachedHidden && !atoken.hiddenRequested)
|| mWinAnimator.mAnimation != null || atoken.animation != null);
} else {
- return mSurface != null && mPolicyVisibility && !mDestroying
+ return mWinAnimator.mSurface != null && mPolicyVisibility && !mDestroying
&& (!mAttachedHidden || mWinAnimator.mAnimation != null);
}
}
@@ -1230,7 +707,7 @@
mService.mNextAppTransition != WindowManagerPolicy.TRANSIT_UNSET) {
return false;
}
- return mSurface != null && mPolicyVisibility && !mDestroying
+ return mWinAnimator.mSurface != null && mPolicyVisibility && !mDestroying
&& ((!mAttachedHidden && mViewVisibility == View.VISIBLE
&& !mRootToken.hidden)
|| mWinAnimator.mAnimation != null
@@ -1264,8 +741,8 @@
* complete UI in to.
*/
public boolean isDrawnLw() {
- return mSurface != null && !mDestroying
- && !mDrawPending && !mCommitDrawPending;
+ return mWinAnimator.mSurface != null && !mDestroying
+ && !mWinAnimator.mDrawPending && !mWinAnimator.mCommitDrawPending;
}
/**
@@ -1300,11 +777,11 @@
disposeInputChannel();
if (mAttachedWindow != null) {
- if (WindowManagerService.DEBUG_ADD_REMOVE) Slog.v(WindowManagerService.TAG, "Removing " + this + " from " + mAttachedWindow);
+ if (WindowManagerService.DEBUG_ADD_REMOVE) Slog.v(TAG, "Removing " + this + " from " + mAttachedWindow);
mAttachedWindow.mChildWindows.remove(this);
}
- destroyDeferredSurfaceLocked();
- destroySurfaceLocked();
+ mWinAnimator.destroyDeferredSurfaceLocked();
+ mWinAnimator.destroySurfaceLocked();
mSession.windowRemovedLocked();
try {
mClient.asBinder().unlinkToDeath(mDeathRecipient, 0);
@@ -1339,7 +816,7 @@
try {
synchronized(mService.mWindowMap) {
WindowState win = mService.windowForClientLocked(mSession, mClient, false);
- Slog.i(WindowManagerService.TAG, "WIN DEATH: " + win);
+ Slog.i(TAG, "WIN DEATH: " + win);
if (win != null) {
mService.removeWindowLocked(mSession, win);
}
@@ -1371,9 +848,9 @@
// Already showing.
return false;
}
- if (DEBUG_VISIBILITY) Slog.v(WindowManagerService.TAG, "Policy visibility true: " + this);
+ if (DEBUG_VISIBILITY) Slog.v(TAG, "Policy visibility true: " + this);
if (doAnimation) {
- if (DEBUG_VISIBILITY) Slog.v(WindowManagerService.TAG, "doAnimation: mPolicyVisibility="
+ if (DEBUG_VISIBILITY) Slog.v(TAG, "doAnimation: mPolicyVisibility="
+ mPolicyVisibility + " mAnimation=" + mWinAnimator.mAnimation);
if (!mService.okToDisplay()) {
doAnimation = false;
@@ -1387,7 +864,7 @@
mPolicyVisibility = true;
mPolicyVisibilityAfterAnim = true;
if (doAnimation) {
- mService.applyAnimationLocked(this, WindowManagerPolicy.TRANSIT_ENTER, true);
+ mWinAnimator.applyAnimationLocked(WindowManagerPolicy.TRANSIT_ENTER, true);
}
if (requestAnim) {
mService.scheduleAnimationLocked();
@@ -1412,7 +889,7 @@
return false;
}
if (doAnimation) {
- mService.applyAnimationLocked(this, WindowManagerPolicy.TRANSIT_EXIT, false);
+ mWinAnimator.applyAnimationLocked(WindowManagerPolicy.TRANSIT_EXIT, false);
if (mWinAnimator.mAnimation == null) {
doAnimation = false;
}
@@ -1420,7 +897,7 @@
if (doAnimation) {
mPolicyVisibilityAfterAnim = false;
} else {
- if (DEBUG_VISIBILITY) Slog.v(WindowManagerService.TAG, "Policy visibility false: " + this);
+ if (DEBUG_VISIBILITY) Slog.v(TAG, "Policy visibility false: " + this);
mPolicyVisibilityAfterAnim = false;
mPolicyVisibility = false;
// Window is no longer visible -- make sure if we were waiting
@@ -1492,24 +969,8 @@
pw.print(" mAnimLayer="); pw.print(mLayer); pw.print("+");
pw.print((mTargetAppToken != null ? mTargetAppToken.animLayerAdjustment
: (mAppToken != null ? mAppToken.animLayerAdjustment : 0)));
- pw.print("="); pw.print(mAnimLayer);
- pw.print(" mLastLayer="); pw.println(mLastLayer);
- }
- if (mSurface != null) {
- if (dumpAll) {
- pw.print(prefix); pw.print("mSurface="); pw.println(mSurface);
- }
- pw.print(prefix); pw.print("Surface: shown="); pw.print(mSurfaceShown);
- pw.print(" layer="); pw.print(mSurfaceLayer);
- pw.print(" alpha="); pw.print(mSurfaceAlpha);
- pw.print(" rect=("); pw.print(mSurfaceX);
- pw.print(","); pw.print(mSurfaceY);
- pw.print(") "); pw.print(mSurfaceW);
- pw.print(" x "); pw.println(mSurfaceH);
- }
- if (mPendingDestroySurface != null) {
- pw.print(prefix); pw.print("mPendingDestroySurface=");
- pw.println(mPendingDestroySurface);
+ pw.print("="); pw.print(mWinAnimator.mAnimLayer);
+ pw.print(" mLastLayer="); pw.println(mWinAnimator.mLastLayer);
}
if (dumpAll) {
pw.print(prefix); pw.print("mToken="); pw.println(mToken);
@@ -1540,10 +1001,6 @@
pw.print(prefix); pw.print("mRelayoutCalled="); pw.print(mRelayoutCalled);
pw.print(" mLayoutNeeded="); pw.println(mLayoutNeeded);
}
- if (mSurfaceResized || mSurfaceDestroyDeferred) {
- pw.print(prefix); pw.print("mSurfaceResized="); pw.print(mSurfaceResized);
- pw.print(" mSurfaceDestroyDeferred="); pw.println(mSurfaceDestroyDeferred);
- }
if (mXOffset != 0 || mYOffset != 0) {
pw.print(prefix); pw.print("Offsets x="); pw.print(mXOffset);
pw.print(" y="); pw.println(mYOffset);
@@ -1589,21 +1046,9 @@
pw.println();
}
mWinAnimator.dump(pw, prefix, dumpAll);
- if (mShownAlpha != 1 || mAlpha != 1 || mLastAlpha != 1) {
- pw.print(prefix); pw.print("mShownAlpha="); pw.print(mShownAlpha);
- pw.print(" mAlpha="); pw.print(mAlpha);
- pw.print(" mLastAlpha="); pw.println(mLastAlpha);
- }
- if (mHaveMatrix || mGlobalScale != 1) {
- pw.print(prefix); pw.print("mGlobalScale="); pw.print(mGlobalScale);
- pw.print(" mDsDx="); pw.print(mDsDx);
- pw.print(" mDtDx="); pw.print(mDtDx);
- pw.print(" mDsDy="); pw.print(mDsDy);
- pw.print(" mDtDy="); pw.println(mDtDy);
- }
if (dumpAll) {
- pw.print(prefix); pw.print("mDrawPending="); pw.print(mDrawPending);
- pw.print(" mCommitDrawPending="); pw.print(mCommitDrawPending);
+ pw.print(prefix); pw.print("mDrawPending="); pw.print(mWinAnimator.mDrawPending);
+ pw.print(" mCommitDrawPending="); pw.print(mWinAnimator.mCommitDrawPending);
pw.print(" mReadyToShow="); pw.print(mReadyToShow);
pw.print(" mHasDrawn="); pw.println(mHasDrawn);
}
diff --git a/services/java/com/android/server/wm/WindowStateAnimator.java b/services/java/com/android/server/wm/WindowStateAnimator.java
index d86d411..e99340c 100644
--- a/services/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/java/com/android/server/wm/WindowStateAnimator.java
@@ -2,10 +2,21 @@
package com.android.server.wm;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.RemoteException;
import android.util.Slog;
+import android.view.Surface;
import android.view.WindowManager;
import android.view.WindowManagerPolicy;
+import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
import android.view.animation.Transformation;
import com.android.server.wm.WindowManagerService.H;
@@ -13,14 +24,27 @@
import java.io.PrintWriter;
/**
- * @author cmautner@google.com (Craig Mautner)
- *
- */
+ * Keep track of animations and surface operations for a single WindowState.
+ **/
class WindowStateAnimator {
+ static final boolean DEBUG_VISIBILITY = WindowManagerService.DEBUG_VISIBILITY;
+ static final boolean DEBUG_ANIM = WindowManagerService.DEBUG_ANIM;
+ static final boolean DEBUG_LAYERS = WindowManagerService.DEBUG_LAYERS;
+ static final boolean DEBUG_STARTING_WINDOW = WindowManagerService.DEBUG_STARTING_WINDOW;
+ static final boolean SHOW_TRANSACTIONS = WindowManagerService.SHOW_TRANSACTIONS;
+ static final boolean SHOW_LIGHT_TRANSACTIONS = WindowManagerService.SHOW_LIGHT_TRANSACTIONS;
+ static final boolean SHOW_SURFACE_ALLOC = WindowManagerService.SHOW_SURFACE_ALLOC;
+ static final boolean localLOGV = WindowManagerService.localLOGV;
+
+ static final String TAG = "WindowStateAnimator";
final WindowManagerService mService;
final WindowState mWin;
final WindowState mAttachedWindow;
+ final WindowAnimator mAnimator;
+ final Session mSession;
+ final WindowManagerPolicy mPolicy;
+ final Context mContext;
// Currently running animation.
boolean mAnimating;
@@ -31,17 +55,72 @@
boolean mHasLocalTransformation;
final Transformation mTransformation = new Transformation();
boolean mWasAnimating; // Were we animating going into the most recent animation step?
+ int mAnimLayer;
+ int mLastLayer;
+
+ Surface mSurface;
+ Surface mPendingDestroySurface;
+ boolean mReportDestroySurface;
+ boolean mSurfacePendingDestroy;
+
+ /**
+ * Set when we have changed the size of the surface, to know that
+ * we must tell them application to resize (and thus redraw itself).
+ */
+ boolean mSurfaceResized;
+
+ /**
+ * Set if the client has asked that the destroy of its surface be delayed
+ * until it explicitly says it is okay.
+ */
+ boolean mSurfaceDestroyDeferred;
+
+ float mShownAlpha = 1;
+ float mAlpha = 1;
+ float mLastAlpha = 1;
+
+ // Used to save animation distances between the time they are calculated and when they are
+ // used.
+ int mAnimDw;
+ int mAnimDh;
+ float mDsDx=1, mDtDx=0, mDsDy=0, mDtDy=1;
+ float mLastDsDx=1, mLastDtDx=0, mLastDsDy=0, mLastDtDy=1;
+
+ boolean mHaveMatrix;
+
+ // For debugging, this is the last information given to the surface flinger.
+ boolean mSurfaceShown;
+ float mSurfaceX, mSurfaceY, mSurfaceW, mSurfaceH;
+ int mSurfaceLayer;
+ float mSurfaceAlpha;
+
+ // Set to true if, when the window gets displayed, it should perform
+ // an enter animation.
+ boolean mEnterAnimationPending;
+
+ // This is set after the Surface has been created but before the
+ // window has been drawn. During this time the surface is hidden.
+ boolean mDrawPending;
+
+ // This is set after the window has finished drawing for the first
+ // time but before its surface is shown. The surface will be
+ // displayed when the next layout is run.
+ boolean mCommitDrawPending;
public WindowStateAnimator(final WindowManagerService service, final WindowState win,
final WindowState attachedWindow) {
mService = service;
mWin = win;
mAttachedWindow = attachedWindow;
+ mAnimator = mService.mAnimator;
+ mSession = win.mSession;
+ mPolicy = mService.mPolicy;
+ mContext = mService.mContext;
}
public void setAnimation(Animation anim) {
- if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "Setting animation in " + this + ": " + anim);
+ if (localLOGV) Slog.v(
+ TAG, "Setting animation in " + this + ": " + anim);
mAnimating = false;
mLocalAnimating = false;
mAnimation = anim;
@@ -78,14 +157,12 @@
return mAnimation != null;
}
- // TODO: Fix and call finishExit() instead of cancelExitAnimationForNextAnimationLocked()
- // for avoiding the code duplication.
void cancelExitAnimationForNextAnimationLocked() {
if (!mWin.mExiting) return;
if (mAnimation != null) {
mAnimation.cancel();
mAnimation = null;
- mWin.destroySurfaceLocked();
+ destroySurfaceLocked();
}
mWin.mExiting = false;
}
@@ -96,8 +173,8 @@
}
mTransformation.clear();
final boolean more = mAnimation.getTransformation(currentTime, mTransformation);
- if (WindowManagerService.DEBUG_ANIM) Slog.v(
- WindowManagerService.TAG, "Stepped animation in " + this +
+ if (DEBUG_ANIM) Slog.v(
+ TAG, "Stepped animation in " + this +
": more=" + more + ", xform=" + mTransformation);
return more;
}
@@ -115,14 +192,14 @@
mHasTransformation = true;
mHasLocalTransformation = true;
if (!mLocalAnimating) {
- if (WindowManagerService.DEBUG_ANIM) Slog.v(
- WindowManagerService.TAG, "Starting animation in " + this +
+ if (DEBUG_ANIM) Slog.v(
+ TAG, "Starting animation in " + this +
" @ " + currentTime + ": ww=" + mWin.mFrame.width() +
" wh=" + mWin.mFrame.height() +
- " dw=" + mWin.mAnimDw + " dh=" + mWin.mAnimDh +
+ " dw=" + mAnimDw + " dh=" + mAnimDh +
" scale=" + mService.mWindowAnimationScale);
- mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(), mWin.mAnimDw,
- mWin.mAnimDh);
+ mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(),
+ mAnimDw, mAnimDh);
mAnimation.setStartTime(currentTime);
mLocalAnimating = true;
mAnimating = true;
@@ -132,8 +209,8 @@
return true;
}
}
- if (WindowManagerService.DEBUG_ANIM) Slog.v(
- WindowManagerService.TAG, "Finished animation in " + this +
+ if (DEBUG_ANIM) Slog.v(
+ TAG, "Finished animation in " + this +
" @ " + currentTime);
//WindowManagerService.this.dump();
}
@@ -171,8 +248,9 @@
return false;
}
- if (WindowManagerService.DEBUG_ANIM) Slog.v(
- WindowManagerService.TAG, "Animation done in " + this + ": exiting=" + mWin.mExiting
+ // Done animating, clean up.
+ if (DEBUG_ANIM) Slog.v(
+ TAG, "Animation done in " + this + ": exiting=" + mWin.mExiting
+ ", reportedVisible="
+ (mWin.mAppToken != null ? mWin.mAppToken.reportedVisible : false));
@@ -182,22 +260,22 @@
mAnimation.cancel();
mAnimation = null;
}
- if (mService.mWindowDetachedWallpaper == mWin) {
- mService.mWindowDetachedWallpaper = null;
+ if (mAnimator.mWindowDetachedWallpaper == mWin) {
+ mAnimator.mWindowDetachedWallpaper = null;
}
- mWin.mAnimLayer = mWin.mLayer;
+ mAnimLayer = mWin.mLayer;
if (mWin.mIsImWindow) {
- mWin.mAnimLayer += mService.mInputMethodAnimLayerAdjustment;
+ mAnimLayer += mService.mInputMethodAnimLayerAdjustment;
} else if (mWin.mIsWallpaper) {
- mWin.mAnimLayer += mService.mWallpaperAnimLayerAdjustment;
+ mAnimLayer += mService.mWallpaperAnimLayerAdjustment;
}
- if (WindowManagerService.DEBUG_LAYERS) Slog.v(WindowManagerService.TAG, "Stepping win " + this
- + " anim layer: " + mWin.mAnimLayer);
+ if (DEBUG_LAYERS) Slog.v(TAG, "Stepping win " + this
+ + " anim layer: " + mAnimLayer);
mHasTransformation = false;
mHasLocalTransformation = false;
if (mWin.mPolicyVisibility != mWin.mPolicyVisibilityAfterAnim) {
if (WindowState.DEBUG_VISIBILITY) {
- Slog.v(WindowManagerService.TAG, "Policy visibility changing after anim in " + this + ": "
+ Slog.v(TAG, "Policy visibility changing after anim in " + this + ": "
+ mWin.mPolicyVisibilityAfterAnim);
}
mWin.mPolicyVisibility = mWin.mPolicyVisibilityAfterAnim;
@@ -218,7 +296,7 @@
&& mWin.mAppToken != null
&& mWin.mAppToken.firstWindowDrawn
&& mWin.mAppToken.startingData != null) {
- if (WindowManagerService.DEBUG_STARTING_WINDOW) Slog.v(WindowManagerService.TAG, "Finish starting "
+ if (DEBUG_STARTING_WINDOW) Slog.v(TAG, "Finish starting "
+ mWin.mToken + ": first real window done animating");
mService.mFinishedStarting.add(mWin.mAppToken);
mService.mH.sendEmptyMessage(H.FINISHED_STARTING);
@@ -237,7 +315,7 @@
void finishExit() {
if (WindowManagerService.DEBUG_ANIM) Slog.v(
- WindowManagerService.TAG, "finishExit in " + this
+ TAG, "finishExit in " + this
+ ": exiting=" + mWin.mExiting
+ " remove=" + mWin.mRemoveOnExit
+ " windowAnimating=" + isWindowAnimating());
@@ -256,18 +334,18 @@
}
if (WindowManagerService.localLOGV) Slog.v(
- WindowManagerService.TAG, "Exit animation finished in " + this
+ TAG, "Exit animation finished in " + this
+ ": remove=" + mWin.mRemoveOnExit);
- if (mWin.mSurface != null) {
+ if (mSurface != null) {
mService.mDestroySurface.add(mWin);
mWin.mDestroying = true;
if (WindowState.SHOW_TRANSACTIONS) WindowManagerService.logSurface(
mWin, "HIDE (finishExit)", null);
- mWin.mSurfaceShown = false;
+ mSurfaceShown = false;
try {
- mWin.mSurface.hide();
+ mSurface.hide();
} catch (RuntimeException e) {
- Slog.w(WindowManagerService.TAG, "Error hiding surface in " + this, e);
+ Slog.w(TAG, "Error hiding surface in " + this, e);
}
mWin.mLastHidden = true;
}
@@ -278,6 +356,770 @@
}
}
+ boolean finishDrawingLocked() {
+ if (mDrawPending) {
+ if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION) Slog.v(
+ TAG, "finishDrawingLocked: " + this + " in " + mSurface);
+ mCommitDrawPending = true;
+ mDrawPending = false;
+ return true;
+ }
+ return false;
+ }
+
+ // This must be called while inside a transaction.
+ boolean commitFinishDrawingLocked(long currentTime) {
+ //Slog.i(TAG, "commitFinishDrawingLocked: " + mSurface);
+ if (!mCommitDrawPending) {
+ return false;
+ }
+ mCommitDrawPending = false;
+ mWin.mReadyToShow = true;
+ final boolean starting = mWin.mAttrs.type == TYPE_APPLICATION_STARTING;
+ final AppWindowToken atoken = mWin.mAppToken;
+ if (atoken == null || atoken.allDrawn || starting) {
+ performShowLocked();
+ }
+ return true;
+ }
+
+ Surface createSurfaceLocked() {
+ if (mSurface == null) {
+ mReportDestroySurface = false;
+ mSurfacePendingDestroy = false;
+ if (WindowManagerService.DEBUG_ORIENTATION) Slog.i(TAG,
+ "createSurface " + this + ": DRAW NOW PENDING");
+ mDrawPending = true;
+ mCommitDrawPending = false;
+ mWin.mReadyToShow = false;
+ if (mWin.mAppToken != null) {
+ mWin.mAppToken.allDrawn = false;
+ }
+
+ mService.makeWindowFreezingScreenIfNeededLocked(mWin);
+
+ int flags = 0;
+ final WindowManager.LayoutParams attrs = mWin.mAttrs;
+
+ if ((attrs.flags&WindowManager.LayoutParams.FLAG_SECURE) != 0) {
+ flags |= Surface.SECURE;
+ }
+ if (WindowState.DEBUG_VISIBILITY) Slog.v(
+ TAG, "Creating surface in session "
+ + mSession.mSurfaceSession + " window " + this
+ + " w=" + mWin.mCompatFrame.width()
+ + " h=" + mWin.mCompatFrame.height() + " format="
+ + attrs.format + " flags=" + flags);
+
+ int w = mWin.mCompatFrame.width();
+ int h = mWin.mCompatFrame.height();
+ if ((attrs.flags & LayoutParams.FLAG_SCALED) != 0) {
+ // for a scaled surface, we always want the requested
+ // size.
+ w = mWin.mRequestedWidth;
+ h = mWin.mRequestedHeight;
+ }
+
+ // Something is wrong and SurfaceFlinger will not like this,
+ // try to revert to sane values
+ if (w <= 0) w = 1;
+ if (h <= 0) h = 1;
+
+ mSurfaceShown = false;
+ mSurfaceLayer = 0;
+ mSurfaceAlpha = 1;
+ mSurfaceX = 0;
+ mSurfaceY = 0;
+ mSurfaceW = w;
+ mSurfaceH = h;
+ try {
+ final boolean isHwAccelerated = (attrs.flags &
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
+ final int format = isHwAccelerated ? PixelFormat.TRANSLUCENT : attrs.format;
+ if (!PixelFormat.formatHasAlpha(attrs.format)) {
+ flags |= Surface.OPAQUE;
+ }
+ mSurface = new Surface(
+ mSession.mSurfaceSession, mSession.mPid,
+ attrs.getTitle().toString(),
+ 0, w, h, format, flags);
+ if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) Slog.i(TAG,
+ " CREATE SURFACE "
+ + mSurface + " IN SESSION "
+ + mSession.mSurfaceSession
+ + ": pid=" + mSession.mPid + " format="
+ + attrs.format + " flags=0x"
+ + Integer.toHexString(flags)
+ + " / " + this);
+ } catch (Surface.OutOfResourcesException e) {
+ Slog.w(TAG, "OutOfResourcesException creating surface");
+ mService.reclaimSomeSurfaceMemoryLocked(this, "create", true);
+ return null;
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception creating surface", e);
+ return null;
+ }
+
+ if (WindowManagerService.localLOGV) Slog.v(
+ TAG, "Got surface: " + mSurface
+ + ", set left=" + mWin.mFrame.left + " top=" + mWin.mFrame.top
+ + ", animLayer=" + mAnimLayer);
+ if (SHOW_LIGHT_TRANSACTIONS) {
+ Slog.i(TAG, ">>> OPEN TRANSACTION createSurfaceLocked");
+ WindowManagerService.logSurface(mWin, "CREATE pos=("
+ + mWin.mFrame.left + "," + mWin.mFrame.top + ") ("
+ + mWin.mCompatFrame.width() + "x" + mWin.mCompatFrame.height()
+ + "), layer=" + mAnimLayer + " HIDE", null);
+ }
+ Surface.openTransaction();
+ try {
+ try {
+ mSurfaceX = mWin.mFrame.left + mWin.mXOffset;
+ mSurfaceY = mWin.mFrame.top + mWin.mYOffset;
+ mSurface.setPosition(mSurfaceX, mSurfaceY);
+ mSurfaceLayer = mAnimLayer;
+ mSurface.setLayer(mAnimLayer);
+ mSurfaceShown = false;
+ mSurface.hide();
+ if ((mWin.mAttrs.flags&WindowManager.LayoutParams.FLAG_DITHER) != 0) {
+ if (SHOW_TRANSACTIONS) WindowManagerService.logSurface(mWin, "DITHER", null);
+ mSurface.setFlags(Surface.SURFACE_DITHER, Surface.SURFACE_DITHER);
+ }
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Error creating surface in " + w, e);
+ mService.reclaimSomeSurfaceMemoryLocked(this, "create-init", true);
+ }
+ mWin.mLastHidden = true;
+ } finally {
+ Surface.closeTransaction();
+ if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
+ "<<< CLOSE TRANSACTION createSurfaceLocked");
+ }
+ if (WindowManagerService.localLOGV) Slog.v(
+ TAG, "Created surface " + this);
+ }
+ return mSurface;
+ }
+
+ void destroySurfaceLocked() {
+ if (mWin.mAppToken != null && mWin == mWin.mAppToken.startingWindow) {
+ mWin.mAppToken.startingDisplayed = false;
+ }
+
+ if (mSurface != null) {
+ mDrawPending = false;
+ mCommitDrawPending = false;
+ mWin.mReadyToShow = false;
+
+ int i = mWin.mChildWindows.size();
+ while (i > 0) {
+ i--;
+ WindowState c = mWin.mChildWindows.get(i);
+ c.mAttachedHidden = true;
+ }
+
+ if (mReportDestroySurface) {
+ mReportDestroySurface = false;
+ mSurfacePendingDestroy = true;
+ try {
+ mWin.mClient.dispatchGetNewSurface();
+ // We'll really destroy on the next time around.
+ return;
+ } catch (RemoteException e) {
+ }
+ }
+
+ try {
+ if (DEBUG_VISIBILITY) {
+ RuntimeException e = null;
+ if (!WindowManagerService.HIDE_STACK_CRAWLS) {
+ e = new RuntimeException();
+ e.fillInStackTrace();
+ }
+ Slog.w(TAG, "Window " + this + " destroying surface "
+ + mSurface + ", session " + mSession, e);
+ }
+ if (mSurfaceDestroyDeferred) {
+ if (mSurface != null && mPendingDestroySurface != mSurface) {
+ if (mPendingDestroySurface != null) {
+ if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) {
+ RuntimeException e = null;
+ if (!WindowManagerService.HIDE_STACK_CRAWLS) {
+ e = new RuntimeException();
+ e.fillInStackTrace();
+ }
+ WindowManagerService.logSurface(mWin, "DESTROY PENDING", e);
+ }
+ mPendingDestroySurface.destroy();
+ }
+ mPendingDestroySurface = mSurface;
+ }
+ } else {
+ if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) {
+ RuntimeException e = null;
+ if (!WindowManagerService.HIDE_STACK_CRAWLS) {
+ e = new RuntimeException();
+ e.fillInStackTrace();
+ }
+ WindowManagerService.logSurface(mWin, "DESTROY", e);
+ }
+ mSurface.destroy();
+ }
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Exception thrown when destroying Window " + this
+ + " surface " + mSurface + " session " + mSession
+ + ": " + e.toString());
+ }
+
+ mSurfaceShown = false;
+ mSurface = null;
+ }
+ }
+
+ void destroyDeferredSurfaceLocked() {
+ try {
+ if (mPendingDestroySurface != null) {
+ if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) {
+ RuntimeException e = null;
+ if (!WindowManagerService.HIDE_STACK_CRAWLS) {
+ e = new RuntimeException();
+ e.fillInStackTrace();
+ }
+ WindowManagerService.logSurface(mWin, "DESTROY PENDING", e);
+ }
+ mPendingDestroySurface.destroy();
+ }
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Exception thrown when destroying Window "
+ + this + " surface " + mPendingDestroySurface
+ + " session " + mSession + ": " + e.toString());
+ }
+ mSurfaceDestroyDeferred = false;
+ mPendingDestroySurface = null;
+ }
+
+ void computeShownFrameLocked() {
+ final boolean selfTransformation = mHasLocalTransformation;
+ Transformation attachedTransformation =
+ (mAttachedWindow != null && mAttachedWindow.mWinAnimator.mHasLocalTransformation)
+ ? mAttachedWindow.mWinAnimator.mTransformation : null;
+ Transformation appTransformation =
+ (mWin.mAppToken != null && mWin.mAppToken.hasTransformation)
+ ? mWin.mAppToken.transformation : null;
+
+ // Wallpapers are animated based on the "real" window they
+ // are currently targeting.
+ if (mWin.mAttrs.type == TYPE_WALLPAPER && mService.mLowerWallpaperTarget == null
+ && mService.mWallpaperTarget != null) {
+ if (mService.mWallpaperTarget.mWinAnimator.mHasLocalTransformation &&
+ mService.mWallpaperTarget.mWinAnimator.mAnimation != null &&
+ !mService.mWallpaperTarget.mWinAnimator.mAnimation.getDetachWallpaper()) {
+ attachedTransformation = mService.mWallpaperTarget.mWinAnimator.mTransformation;
+ if (WindowManagerService.DEBUG_WALLPAPER && attachedTransformation != null) {
+ Slog.v(TAG, "WP target attached xform: " + attachedTransformation);
+ }
+ }
+ if (mService.mWallpaperTarget.mAppToken != null &&
+ mService.mWallpaperTarget.mAppToken.hasTransformation &&
+ mService.mWallpaperTarget.mAppToken.animation != null &&
+ !mService.mWallpaperTarget.mAppToken.animation.getDetachWallpaper()) {
+ appTransformation = mService.mWallpaperTarget.mAppToken.transformation;
+ if (WindowManagerService.DEBUG_WALLPAPER && appTransformation != null) {
+ Slog.v(TAG, "WP target app xform: " + appTransformation);
+ }
+ }
+ }
+
+ final boolean screenAnimation = mService.mAnimator.mScreenRotationAnimation != null
+ && mService.mAnimator.mScreenRotationAnimation.isAnimating();
+ if (selfTransformation || attachedTransformation != null
+ || appTransformation != null || screenAnimation) {
+ // cache often used attributes locally
+ final Rect frame = mWin.mFrame;
+ final float tmpFloats[] = mService.mTmpFloats;
+ final Matrix tmpMatrix = mWin.mTmpMatrix;
+
+ // Compute the desired transformation.
+ if (screenAnimation) {
+ // If we are doing a screen animation, the global rotation
+ // applied to windows can result in windows that are carefully
+ // aligned with each other to slightly separate, allowing you
+ // to see what is behind them. An unsightly mess. This...
+ // thing... magically makes it call good: scale each window
+ // slightly (two pixels larger in each dimension, from the
+ // window's center).
+ final float w = frame.width();
+ final float h = frame.height();
+ if (w>=1 && h>=1) {
+ tmpMatrix.setScale(1 + 2/w, 1 + 2/h, w/2, h/2);
+ } else {
+ tmpMatrix.reset();
+ }
+ } else {
+ tmpMatrix.reset();
+ }
+ tmpMatrix.postScale(mWin.mGlobalScale, mWin.mGlobalScale);
+ if (selfTransformation) {
+ tmpMatrix.postConcat(mTransformation.getMatrix());
+ }
+ tmpMatrix.postTranslate(frame.left + mWin.mXOffset, frame.top + mWin.mYOffset);
+ if (attachedTransformation != null) {
+ tmpMatrix.postConcat(attachedTransformation.getMatrix());
+ }
+ if (appTransformation != null) {
+ tmpMatrix.postConcat(appTransformation.getMatrix());
+ }
+ if (screenAnimation) {
+ tmpMatrix.postConcat(
+ mService.mAnimator.mScreenRotationAnimation.getEnterTransformation().getMatrix());
+ }
+
+ // "convert" it into SurfaceFlinger's format
+ // (a 2x2 matrix + an offset)
+ // Here we must not transform the position of the surface
+ // since it is already included in the transformation.
+ //Slog.i(TAG, "Transform: " + matrix);
+
+ mHaveMatrix = true;
+ tmpMatrix.getValues(tmpFloats);
+ mDsDx = tmpFloats[Matrix.MSCALE_X];
+ mDtDx = tmpFloats[Matrix.MSKEW_Y];
+ mDsDy = tmpFloats[Matrix.MSKEW_X];
+ mDtDy = tmpFloats[Matrix.MSCALE_Y];
+ float x = tmpFloats[Matrix.MTRANS_X];
+ float y = tmpFloats[Matrix.MTRANS_Y];
+ int w = frame.width();
+ int h = frame.height();
+ mWin.mShownFrame.set(x, y, x+w, y+h);
+
+ // Now set the alpha... but because our current hardware
+ // can't do alpha transformation on a non-opaque surface,
+ // turn it off if we are running an animation that is also
+ // transforming since it is more important to have that
+ // animation be smooth.
+ mShownAlpha = mAlpha;
+ if (!mService.mLimitedAlphaCompositing
+ || (!PixelFormat.formatHasAlpha(mWin.mAttrs.format)
+ || (mWin.isIdentityMatrix(mDsDx, mDtDx, mDsDy, mDtDy)
+ && x == frame.left && y == frame.top))) {
+ //Slog.i(TAG, "Applying alpha transform");
+ if (selfTransformation) {
+ mShownAlpha *= mTransformation.getAlpha();
+ }
+ if (attachedTransformation != null) {
+ mShownAlpha *= attachedTransformation.getAlpha();
+ }
+ if (appTransformation != null) {
+ mShownAlpha *= appTransformation.getAlpha();
+ }
+ if (screenAnimation) {
+ mShownAlpha *=
+ mService.mAnimator.mScreenRotationAnimation.getEnterTransformation().getAlpha();
+ }
+ } else {
+ //Slog.i(TAG, "Not applying alpha transform");
+ }
+
+ if (WindowManagerService.localLOGV) Slog.v(
+ TAG, "computeShownFrameLocked: Animating " + this +
+ ": " + mWin.mShownFrame +
+ ", alpha=" + mTransformation.getAlpha() + ", mShownAlpha=" + mShownAlpha);
+ return;
+ }
+
+ if (WindowManagerService.localLOGV) Slog.v(
+ TAG, "computeShownFrameLocked: " + this +
+ " not attached, mAlpha=" + mAlpha);
+ mWin.mShownFrame.set(mWin.mFrame);
+ if (mWin.mXOffset != 0 || mWin.mYOffset != 0) {
+ mWin.mShownFrame.offset(mWin.mXOffset, mWin.mYOffset);
+ }
+ mShownAlpha = mAlpha;
+ mHaveMatrix = false;
+ mDsDx = mWin.mGlobalScale;
+ mDtDx = 0;
+ mDsDy = 0;
+ mDtDy = mWin.mGlobalScale;
+ }
+
+ public void prepareSurfaceLocked(final boolean recoveringMemory) {
+ final WindowState w = mWin;
+ if (mSurface == null) {
+ if (w.mOrientationChanging) {
+ if (WindowManagerService.DEBUG_ORIENTATION) {
+ Slog.v(TAG, "Orientation change skips hidden " + w);
+ }
+ w.mOrientationChanging = false;
+ }
+ return;
+ }
+
+ boolean displayed = false;
+
+ computeShownFrameLocked();
+
+ int width, height;
+ if ((w.mAttrs.flags & LayoutParams.FLAG_SCALED) != 0) {
+ // for a scaled surface, we just want to use
+ // the requested size.
+ width = w.mRequestedWidth;
+ height = w.mRequestedHeight;
+ } else {
+ width = w.mCompatFrame.width();
+ height = w.mCompatFrame.height();
+ }
+
+ if (width < 1) {
+ width = 1;
+ }
+ if (height < 1) {
+ height = 1;
+ }
+ final boolean surfaceResized = mSurfaceW != width || mSurfaceH != height;
+ if (surfaceResized) {
+ mSurfaceW = width;
+ mSurfaceH = height;
+ }
+
+ if (mSurfaceX != w.mShownFrame.left
+ || mSurfaceY != w.mShownFrame.top) {
+ try {
+ if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
+ "POS " + w.mShownFrame.left
+ + ", " + w.mShownFrame.top, null);
+ mSurfaceX = w.mShownFrame.left;
+ mSurfaceY = w.mShownFrame.top;
+ mSurface.setPosition(w.mShownFrame.left, w.mShownFrame.top);
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Error positioning surface of " + w
+ + " pos=(" + w.mShownFrame.left
+ + "," + w.mShownFrame.top + ")", e);
+ if (!recoveringMemory) {
+ mService.reclaimSomeSurfaceMemoryLocked(this, "position", true);
+ }
+ }
+ }
+
+ if (surfaceResized) {
+ try {
+ if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
+ "SIZE " + width + "x" + height, null);
+ mSurfaceResized = true;
+ mSurface.setSize(width, height);
+ } catch (RuntimeException e) {
+ // If something goes wrong with the surface (such
+ // as running out of memory), don't take down the
+ // entire system.
+ Slog.e(TAG, "Error resizing surface of " + w
+ + " size=(" + width + "x" + height + ")", e);
+ if (!recoveringMemory) {
+ mService.reclaimSomeSurfaceMemoryLocked(this, "size", true);
+ }
+ }
+ }
+
+ if (w.mAttachedHidden || !w.isReadyForDisplay()) {
+ if (!w.mLastHidden) {
+ //dump();
+ w.mLastHidden = true;
+ if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
+ "HIDE (performLayout)", null);
+ if (mSurface != null) {
+ mSurfaceShown = false;
+ try {
+ mSurface.hide();
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Exception hiding surface in " + w);
+ }
+ }
+ }
+ // If we are waiting for this window to handle an
+ // orientation change, well, it is hidden, so
+ // doesn't really matter. Note that this does
+ // introduce a potential glitch if the window
+ // becomes unhidden before it has drawn for the
+ // new orientation.
+ if (w.mOrientationChanging) {
+ w.mOrientationChanging = false;
+ if (WindowManagerService.DEBUG_ORIENTATION) Slog.v(TAG,
+ "Orientation change skips hidden " + w);
+ }
+ } else if (mLastLayer != mAnimLayer
+ || mLastAlpha != mShownAlpha
+ || mLastDsDx != mDsDx
+ || mLastDtDx != mDtDx
+ || mLastDsDy != mDsDy
+ || mLastDtDy != mDtDy
+ || w.mLastHScale != w.mHScale
+ || w.mLastVScale != w.mVScale
+ || w.mLastHidden) {
+ displayed = true;
+ mLastAlpha = mShownAlpha;
+ mLastLayer = mAnimLayer;
+ mLastDsDx = mDsDx;
+ mLastDtDx = mDtDx;
+ mLastDsDy = mDsDy;
+ mLastDtDy = mDtDy;
+ w.mLastHScale = w.mHScale;
+ w.mLastVScale = w.mVScale;
+ if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
+ "alpha=" + mShownAlpha + " layer=" + mAnimLayer
+ + " matrix=[" + (mDsDx*w.mHScale)
+ + "," + (mDtDx*w.mVScale)
+ + "][" + (mDsDy*w.mHScale)
+ + "," + (mDtDy*w.mVScale) + "]", null);
+ if (mSurface != null) {
+ try {
+ mSurfaceAlpha = mShownAlpha;
+ mSurface.setAlpha(mShownAlpha);
+ mSurfaceLayer = w.mWinAnimator.mAnimLayer;
+ mSurface.setLayer(w.mWinAnimator.mAnimLayer);
+ mSurface.setMatrix(
+ mDsDx*w.mHScale, mDtDx*w.mVScale,
+ mDsDy*w.mHScale, mDtDy*w.mVScale);
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Error updating surface in " + w, e);
+ if (!recoveringMemory) {
+ mService.reclaimSomeSurfaceMemoryLocked(this, "update", true);
+ }
+ }
+ }
+
+ if (w.mLastHidden && w.isDrawnLw()
+ && !w.mReadyToShow) {
+ if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
+ "SHOW (performLayout)", null);
+ if (WindowManagerService.DEBUG_VISIBILITY) Slog.v(TAG, "Showing " + w
+ + " during relayout");
+ if (showSurfaceRobustlyLocked()) {
+ w.mHasDrawn = true;
+ w.mLastHidden = false;
+ } else {
+ w.mOrientationChanging = false;
+ }
+ }
+ if (mSurface != null) {
+ w.mToken.hasVisible = true;
+ }
+ } else {
+ displayed = true;
+ }
+
+ if (displayed) {
+ if (w.mOrientationChanging) {
+ if (!w.isDrawnLw()) {
+ mService.mInnerFields.mOrientationChangeComplete = false;
+ if (WindowManagerService.DEBUG_ORIENTATION) Slog.v(TAG,
+ "Orientation continue waiting for draw in " + w);
+ } else {
+ w.mOrientationChanging = false;
+ if (WindowManagerService.DEBUG_ORIENTATION) Slog.v(TAG,
+ "Orientation change complete in " + w);
+ }
+ }
+ w.mToken.hasVisible = true;
+ }
+ }
+
+ // This must be called while inside a transaction.
+ boolean performShowLocked() {
+ if (DEBUG_VISIBILITY) {
+ RuntimeException e = null;
+ if (!WindowManagerService.HIDE_STACK_CRAWLS) {
+ e = new RuntimeException();
+ e.fillInStackTrace();
+ }
+ Slog.v(TAG, "performShow on " + this
+ + ": readyToShow=" + mWin.mReadyToShow + " readyForDisplay="
+ + mWin.isReadyForDisplay()
+ + " starting=" + (mWin.mAttrs.type == TYPE_APPLICATION_STARTING), e);
+ }
+ if (mWin.mReadyToShow && mWin.isReadyForDisplay()) {
+ if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION)
+ WindowManagerService.logSurface(mWin, "SHOW (performShowLocked)", null);
+ if (DEBUG_VISIBILITY) Slog.v(TAG, "Showing " + this
+ + " during animation: policyVis=" + mWin.mPolicyVisibility
+ + " attHidden=" + mWin.mAttachedHidden
+ + " tok.hiddenRequested="
+ + (mWin.mAppToken != null ? mWin.mAppToken.hiddenRequested : false)
+ + " tok.hidden="
+ + (mWin.mAppToken != null ? mWin.mAppToken.hidden : false)
+ + " animating=" + mAnimating
+ + " tok animating="
+ + (mWin.mAppToken != null ? mWin.mAppToken.animating : false));
+ if (!showSurfaceRobustlyLocked()) {
+ return false;
+ }
+
+ mService.enableScreenIfNeededLocked();
+
+ applyEnterAnimationLocked();
+
+ mLastAlpha = -1;
+ mWin.mHasDrawn = true;
+ mWin.mLastHidden = false;
+ mWin.mReadyToShow = false;
+
+ int i = mWin.mChildWindows.size();
+ while (i > 0) {
+ i--;
+ WindowState c = mWin.mChildWindows.get(i);
+ if (c.mAttachedHidden) {
+ c.mAttachedHidden = false;
+ if (c.mWinAnimator.mSurface != null) {
+ c.mWinAnimator.performShowLocked();
+ // It hadn't been shown, which means layout not
+ // performed on it, so now we want to make sure to
+ // do a layout. If called from within the transaction
+ // loop, this will cause it to restart with a new
+ // layout.
+ mService.mLayoutNeeded = true;
+ }
+ }
+ }
+
+ if (mWin.mAttrs.type != TYPE_APPLICATION_STARTING
+ && mWin.mAppToken != null) {
+ mWin.mAppToken.firstWindowDrawn = true;
+
+ if (mWin.mAppToken.startingData != null) {
+ if (WindowManagerService.DEBUG_STARTING_WINDOW ||
+ WindowManagerService.DEBUG_ANIM) Slog.v(TAG,
+ "Finish starting " + mWin.mToken
+ + ": first real window is shown, no animation");
+ // If this initial window is animating, stop it -- we
+ // will do an animation to reveal it from behind the
+ // starting window, so there is no need for it to also
+ // be doing its own stuff.
+ if (mAnimation != null) {
+ mAnimation.cancel();
+ mAnimation = null;
+ // Make sure we clean up the animation.
+ mAnimating = true;
+ }
+ mService.mFinishedStarting.add(mWin.mAppToken);
+ mService.mH.sendEmptyMessage(H.FINISHED_STARTING);
+ }
+ mWin.mAppToken.updateReportedVisibilityLocked();
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Have the surface flinger show a surface, robustly dealing with
+ * error conditions. In particular, if there is not enough memory
+ * to show the surface, then we will try to get rid of other surfaces
+ * in order to succeed.
+ *
+ * @return Returns true if the surface was successfully shown.
+ */
+ boolean showSurfaceRobustlyLocked() {
+ try {
+ if (mSurface != null) {
+ mSurfaceShown = true;
+ mSurface.show();
+ if (mWin.mTurnOnScreen) {
+ if (DEBUG_VISIBILITY) Slog.v(TAG,
+ "Show surface turning screen on: " + mWin);
+ mWin.mTurnOnScreen = false;
+ mService.mTurnOnScreen = true;
+ }
+ }
+ return true;
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Failure showing surface " + mSurface + " in " + mWin, e);
+ }
+
+ mService.reclaimSomeSurfaceMemoryLocked(this, "show", true);
+
+ return false;
+ }
+
+ void applyEnterAnimationLocked() {
+ final int transit;
+ if (mEnterAnimationPending) {
+ mEnterAnimationPending = false;
+ transit = WindowManagerPolicy.TRANSIT_ENTER;
+ } else {
+ transit = WindowManagerPolicy.TRANSIT_SHOW;
+ }
+
+ applyAnimationLocked(transit, true);
+ }
+
+ /**
+ * Choose the correct animation and set it to the passed WindowState.
+ * @param transit If WindowManagerPolicy.TRANSIT_PREVIEW_DONE and the app window has been drawn
+ * then the animation will be app_starting_exit. Any other value loads the animation from
+ * the switch statement below.
+ * @param isEntrance The animation type the last time this was called. Used to keep from
+ * loading the same animation twice.
+ * @return true if an animation has been loaded.
+ */
+ boolean applyAnimationLocked(int transit, boolean isEntrance) {
+ if (mLocalAnimating && mAnimationIsEntrance == isEntrance) {
+ // If we are trying to apply an animation, but already running
+ // an animation of the same type, then just leave that one alone.
+ return true;
+ }
+
+ // Only apply an animation if the display isn't frozen. If it is
+ // frozen, there is no reason to animate and it can cause strange
+ // artifacts when we unfreeze the display if some different animation
+ // is running.
+ if (mService.okToDisplay()) {
+ int anim = mPolicy.selectAnimationLw(mWin, transit);
+ int attr = -1;
+ Animation a = null;
+ if (anim != 0) {
+ a = AnimationUtils.loadAnimation(mContext, anim);
+ } else {
+ switch (transit) {
+ case WindowManagerPolicy.TRANSIT_ENTER:
+ attr = com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
+ break;
+ case WindowManagerPolicy.TRANSIT_EXIT:
+ attr = com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
+ break;
+ case WindowManagerPolicy.TRANSIT_SHOW:
+ attr = com.android.internal.R.styleable.WindowAnimation_windowShowAnimation;
+ break;
+ case WindowManagerPolicy.TRANSIT_HIDE:
+ attr = com.android.internal.R.styleable.WindowAnimation_windowHideAnimation;
+ break;
+ }
+ if (attr >= 0) {
+ a = mService.loadAnimation(mWin.mAttrs, attr);
+ }
+ }
+ if (WindowManagerService.DEBUG_ANIM) Slog.v(TAG,
+ "applyAnimation: win=" + this
+ + " anim=" + anim + " attr=0x" + Integer.toHexString(attr)
+ + " mAnimation=" + mAnimation
+ + " isEntrance=" + isEntrance);
+ if (a != null) {
+ if (WindowManagerService.DEBUG_ANIM) {
+ RuntimeException e = null;
+ if (!WindowManagerService.HIDE_STACK_CRAWLS) {
+ e = new RuntimeException();
+ e.fillInStackTrace();
+ }
+ Slog.v(TAG, "Loaded animation " + a + " for " + this, e);
+ }
+ setAnimation(a);
+ mAnimationIsEntrance = isEntrance;
+ }
+ } else {
+ clearAnimation();
+ }
+
+ return mAnimation != null;
+ }
+
public void dump(PrintWriter pw, String prefix, boolean dumpAll) {
if (mAnimating || mLocalAnimating || mAnimationIsEntrance
|| mAnimation != null) {
@@ -293,6 +1135,38 @@
pw.print(" "); mTransformation.printShortString(pw);
pw.println();
}
+ if (mSurface != null) {
+ if (dumpAll) {
+ pw.print(prefix); pw.print("mSurface="); pw.println(mSurface);
+ }
+ pw.print(prefix); pw.print("Surface: shown="); pw.print(mSurfaceShown);
+ pw.print(" layer="); pw.print(mSurfaceLayer);
+ pw.print(" alpha="); pw.print(mSurfaceAlpha);
+ pw.print(" rect=("); pw.print(mSurfaceX);
+ pw.print(","); pw.print(mSurfaceY);
+ pw.print(") "); pw.print(mSurfaceW);
+ pw.print(" x "); pw.println(mSurfaceH);
+ }
+ if (mPendingDestroySurface != null) {
+ pw.print(prefix); pw.print("mPendingDestroySurface=");
+ pw.println(mPendingDestroySurface);
+ }
+ if (mSurfaceResized || mSurfaceDestroyDeferred) {
+ pw.print(prefix); pw.print("mSurfaceResized="); pw.print(mSurfaceResized);
+ pw.print(" mSurfaceDestroyDeferred="); pw.println(mSurfaceDestroyDeferred);
+ }
+ if (mShownAlpha != 1 || mAlpha != 1 || mLastAlpha != 1) {
+ pw.print(prefix); pw.print("mShownAlpha="); pw.print(mShownAlpha);
+ pw.print(" mAlpha="); pw.print(mAlpha);
+ pw.print(" mLastAlpha="); pw.println(mLastAlpha);
+ }
+ if (mHaveMatrix || mWin.mGlobalScale != 1) {
+ pw.print(prefix); pw.print("mGlobalScale="); pw.print(mWin.mGlobalScale);
+ pw.print(" mDsDx="); pw.print(mDsDx);
+ pw.print(" mDtDx="); pw.print(mDtDx);
+ pw.print(" mDsDy="); pw.print(mDsDy);
+ pw.print(" mDtDy="); pw.println(mDtDy);
+ }
}
}
diff --git a/telephony/java/com/android/internal/telephony/TelephonyCapabilities.java b/telephony/java/com/android/internal/telephony/TelephonyCapabilities.java
new file mode 100644
index 0000000..bd94de2
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/TelephonyCapabilities.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony;
+
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+
+/**
+ * Utilities that check if the phone supports specified capabilities.
+ */
+public class TelephonyCapabilities {
+ private static final String LOG_TAG = "TelephonyCapabilities";
+
+ /** This class is never instantiated. */
+ private TelephonyCapabilities() {
+ }
+
+ /**
+ * Return true if the current phone supports ECM ("Emergency Callback
+ * Mode"), which is a feature where the device goes into a special
+ * state for a short period of time after making an outgoing emergency
+ * call.
+ *
+ * (On current devices, that state lasts 5 minutes. It prevents data
+ * usage by other apps, to avoid conflicts with any possible incoming
+ * calls. It also puts up a notification in the status bar, showing a
+ * countdown while ECM is active, and allowing the user to exit ECM.)
+ *
+ * Currently this is assumed to be true for CDMA phones, and false
+ * otherwise.
+ */
+ public static boolean supportsEcm(Phone phone) {
+ return (phone.getPhoneType() == Phone.PHONE_TYPE_CDMA);
+ }
+
+ /**
+ * Return true if the current phone supports Over The Air Service
+ * Provisioning (OTASP)
+ *
+ * Currently this is assumed to be true for CDMA phones, and false
+ * otherwise.
+ *
+ * TODO: Watch out: this is also highly carrier-specific, since the
+ * OTASP procedure is different from one carrier to the next, *and* the
+ * different carriers may want very different onscreen UI as well.
+ * The procedure may even be different for different devices with the
+ * same carrier.
+ *
+ * So we eventually will need a much more flexible, pluggable design.
+ * This method here is just a placeholder to reduce hardcoded
+ * "if (CDMA)" checks sprinkled throughout the phone app.
+ */
+ public static boolean supportsOtasp(Phone phone) {
+ return (phone.getPhoneType() == Phone.PHONE_TYPE_CDMA);
+ }
+
+ /**
+ * Return true if the current phone can retrieve the voice message count.
+ *
+ * Currently this is assumed to be true on CDMA phones and false otherwise.
+ */
+ public static boolean supportsVoiceMessageCount(Phone phone) {
+ return (phone.getPhoneType() == Phone.PHONE_TYPE_CDMA);
+ }
+
+ /**
+ * Return true if this phone allows the user to select which
+ * network to use.
+ *
+ * Currently this is assumed to be true only on GSM phones.
+ *
+ * TODO: Should CDMA phones allow this as well?
+ */
+ public static boolean supportsNetworkSelection(Phone phone) {
+ return (phone.getPhoneType() == Phone.PHONE_TYPE_GSM);
+ }
+
+ /**
+ * Returns a resource ID for a label to use when displaying the
+ * "device id" of the current device. (This is currently used as the
+ * title of the "device id" dialog.)
+ *
+ * This is specific to the device's telephony technology: the device
+ * id is called "IMEI" on GSM phones and "MEID" on CDMA phones.
+ */
+ public static int getDeviceIdLabel(Phone phone) {
+ if (phone.getPhoneType() == Phone.PHONE_TYPE_GSM) {
+ return com.android.internal.R.string.imei;
+ } else if (phone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
+ return com.android.internal.R.string.meid;
+ } else {
+ Log.w(LOG_TAG, "getDeviceIdLabel: no known label for phone "
+ + phone.getPhoneName());
+ return 0;
+ }
+ }
+
+ /**
+ * Return true if the current phone supports the ability to explicitly
+ * manage the state of a conference call (i.e. view the participants,
+ * and hangup or separate individual callers.)
+ *
+ * The in-call screen's "Manage conference" UI is available only on
+ * devices that support this feature.
+ *
+ * Currently this is assumed to be true on GSM phones and false otherwise.
+ */
+ public static boolean supportsConferenceCallManagement(Phone phone) {
+ return ((phone.getPhoneType() == Phone.PHONE_TYPE_GSM)
+ || (phone.getPhoneType() == Phone.PHONE_TYPE_SIP));
+ }
+
+ /**
+ * Return true if the current phone supports explicit "Hold" and
+ * "Unhold" actions for an active call. (If so, the in-call UI will
+ * provide onscreen "Hold" / "Unhold" buttons.)
+ *
+ * Currently this is assumed to be true on GSM phones and false
+ * otherwise. (In particular, CDMA has no concept of "putting a call
+ * on hold.")
+ */
+ public static boolean supportsHoldAndUnhold(Phone phone) {
+ return ((phone.getPhoneType() == Phone.PHONE_TYPE_GSM)
+ || (phone.getPhoneType() == Phone.PHONE_TYPE_SIP));
+ }
+
+ /**
+ * Return true if the current phone supports distinct "Answer & Hold"
+ * and "Answer & End" behaviors in the call-waiting scenario. If so,
+ * the in-call UI may provide separate buttons or menu items for these
+ * two actions.
+ *
+ * Currently this is assumed to be true on GSM phones and false
+ * otherwise. (In particular, CDMA has no concept of explicitly
+ * managing the background call, or "putting a call on hold.")
+ *
+ * TODO: It might be better to expose this capability in a more
+ * generic form, like maybe "supportsExplicitMultipleLineManagement()"
+ * rather than focusing specifically on call-waiting behavior.
+ */
+ public static boolean supportsAnswerAndHold(Phone phone) {
+ return ((phone.getPhoneType() == Phone.PHONE_TYPE_GSM)
+ || (phone.getPhoneType() == Phone.PHONE_TYPE_SIP));
+ }
+
+ /**
+ * Return true if phones with the given phone type support ADN
+ * (Abbreviated Dialing Numbers).
+ *
+ * Currently this returns true when the phone type is GSM
+ * ({@link Phone#PHONE_TYPE_GSM}).
+ *
+ * This is using int for an argument for letting apps outside
+ * Phone process access to it, while other methods in this class is
+ * using Phone object.
+ *
+ * TODO: Theoretically phones other than GSM may have the ADN capability.
+ * Consider having better check here, or have better capability as part
+ * of public API, with which the argument should be replaced with
+ * something more appropriate.
+ */
+ public static boolean supportsAdn(int phoneType) {
+ return phoneType == Phone.PHONE_TYPE_GSM;
+ }
+}
diff --git a/voip/java/android/net/rtp/AudioGroup.java b/voip/java/android/net/rtp/AudioGroup.java
index 3e7ace8..8c19062 100644
--- a/voip/java/android/net/rtp/AudioGroup.java
+++ b/voip/java/android/net/rtp/AudioGroup.java
@@ -142,34 +142,34 @@
private native void nativeSetMode(int mode);
// Package-private method used by AudioStream.join().
- synchronized void add(AudioStream stream, AudioCodec codec, int dtmfType) {
+ synchronized void add(AudioStream stream) {
if (!mStreams.containsKey(stream)) {
try {
- int socket = stream.dup();
+ AudioCodec codec = stream.getCodec();
String codecSpec = String.format("%d %s %s", codec.type,
codec.rtpmap, codec.fmtp);
- nativeAdd(stream.getMode(), socket,
+ int id = nativeAdd(stream.getMode(), stream.getSocket(),
stream.getRemoteAddress().getHostAddress(),
- stream.getRemotePort(), codecSpec, dtmfType);
- mStreams.put(stream, socket);
+ stream.getRemotePort(), codecSpec, stream.getDtmfType());
+ mStreams.put(stream, id);
} catch (NullPointerException e) {
throw new IllegalStateException(e);
}
}
}
- private native void nativeAdd(int mode, int socket, String remoteAddress,
+ private native int nativeAdd(int mode, int socket, String remoteAddress,
int remotePort, String codecSpec, int dtmfType);
// Package-private method used by AudioStream.join().
synchronized void remove(AudioStream stream) {
- Integer socket = mStreams.remove(stream);
- if (socket != null) {
- nativeRemove(socket);
+ Integer id = mStreams.remove(stream);
+ if (id != null) {
+ nativeRemove(id);
}
}
- private native void nativeRemove(int socket);
+ private native void nativeRemove(int id);
/**
* Sends a DTMF digit to every {@link AudioStream} in this group. Currently
@@ -192,15 +192,14 @@
* Removes every {@link AudioStream} in this group.
*/
public void clear() {
- synchronized (this) {
- mStreams.clear();
- nativeRemove(-1);
+ for (AudioStream stream : getStreams()) {
+ stream.join(null);
}
}
@Override
protected void finalize() throws Throwable {
- clear();
+ nativeRemove(0);
super.finalize();
}
}
diff --git a/voip/java/android/net/rtp/AudioStream.java b/voip/java/android/net/rtp/AudioStream.java
index b7874f7..5cd1abc 100644
--- a/voip/java/android/net/rtp/AudioStream.java
+++ b/voip/java/android/net/rtp/AudioStream.java
@@ -94,7 +94,7 @@
mGroup = null;
}
if (group != null) {
- group.add(this, mCodec, mDtmfType);
+ group.add(this);
mGroup = group;
}
}
diff --git a/voip/java/android/net/rtp/RtpStream.java b/voip/java/android/net/rtp/RtpStream.java
index e94ac42..b9d75cd 100644
--- a/voip/java/android/net/rtp/RtpStream.java
+++ b/voip/java/android/net/rtp/RtpStream.java
@@ -54,7 +54,7 @@
private int mRemotePort = -1;
private int mMode = MODE_NORMAL;
- private int mNative;
+ private int mSocket = -1;
static {
System.loadLibrary("rtp_jni");
}
@@ -165,7 +165,9 @@
mRemotePort = port;
}
- synchronized native int dup();
+ int getSocket() {
+ return mSocket;
+ }
/**
* Releases allocated resources. The stream becomes inoperable after calling
@@ -175,13 +177,15 @@
* @see #isBusy()
*/
public void release() {
- if (isBusy()) {
- throw new IllegalStateException("Busy");
+ synchronized (this) {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ close();
}
- close();
}
- private synchronized native void close();
+ private native void close();
@Override
protected void finalize() throws Throwable {
diff --git a/voip/jni/rtp/AudioGroup.cpp b/voip/jni/rtp/AudioGroup.cpp
index b9bbd16..673a650 100644
--- a/voip/jni/rtp/AudioGroup.cpp
+++ b/voip/jni/rtp/AudioGroup.cpp
@@ -478,7 +478,7 @@
bool setMode(int mode);
bool sendDtmf(int event);
bool add(AudioStream *stream);
- bool remove(int socket);
+ bool remove(AudioStream *stream);
bool platformHasAec() { return mPlatformHasAec; }
private:
@@ -691,20 +691,19 @@
return true;
}
-bool AudioGroup::remove(int socket)
+bool AudioGroup::remove(AudioStream *stream)
{
mNetworkThread->requestExitAndWait();
- for (AudioStream *stream = mChain; stream->mNext; stream = stream->mNext) {
- AudioStream *target = stream->mNext;
- if (target->mSocket == socket) {
- if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, socket, NULL)) {
+ for (AudioStream *chain = mChain; chain->mNext; chain = chain->mNext) {
+ if (chain->mNext == stream) {
+ if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, stream->mSocket, NULL)) {
ALOGE("epoll_ctl: %s", strerror(errno));
return false;
}
- stream->mNext = target->mNext;
- ALOGD("stream[%d] leaves group[%d]", socket, mDeviceSocket);
- delete target;
+ chain->mNext = stream->mNext;
+ ALOGD("stream[%d] leaves group[%d]", stream->mSocket, mDeviceSocket);
+ delete stream;
break;
}
}
@@ -931,7 +930,7 @@
static jfieldID gNative;
static jfieldID gMode;
-void add(JNIEnv *env, jobject thiz, jint mode,
+int add(JNIEnv *env, jobject thiz, jint mode,
jint socket, jstring jRemoteAddress, jint remotePort,
jstring jCodecSpec, jint dtmfType)
{
@@ -943,16 +942,22 @@
sockaddr_storage remote;
if (parse(env, jRemoteAddress, remotePort, &remote) < 0) {
// Exception already thrown.
- return;
+ return 0;
}
if (!jCodecSpec) {
jniThrowNullPointerException(env, "codecSpec");
- return;
+ return 0;
}
const char *codecSpec = env->GetStringUTFChars(jCodecSpec, NULL);
if (!codecSpec) {
// Exception already thrown.
- return;
+ return 0;
+ }
+ socket = dup(socket);
+ if (socket == -1) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot get stream socket");
+ return 0;
}
// Create audio codec.
@@ -1001,7 +1006,7 @@
// Succeed.
env->SetIntField(thiz, gNative, (int)group);
- return;
+ return (int)stream;
error:
delete group;
@@ -1009,13 +1014,14 @@
delete codec;
close(socket);
env->SetIntField(thiz, gNative, 0);
+ return 0;
}
-void remove(JNIEnv *env, jobject thiz, jint socket)
+void remove(JNIEnv *env, jobject thiz, jint stream)
{
AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative);
if (group) {
- if (socket == -1 || !group->remove(socket)) {
+ if (!stream || !group->remove((AudioStream *)stream)) {
delete group;
env->SetIntField(thiz, gNative, 0);
}
@@ -1039,7 +1045,7 @@
}
JNINativeMethod gMethods[] = {
- {"nativeAdd", "(IILjava/lang/String;ILjava/lang/String;I)V", (void *)add},
+ {"nativeAdd", "(IILjava/lang/String;ILjava/lang/String;I)I", (void *)add},
{"nativeRemove", "(I)V", (void *)remove},
{"nativeSetMode", "(I)V", (void *)setMode},
{"nativeSendDtmf", "(I)V", (void *)sendDtmf},
diff --git a/voip/jni/rtp/RtpStream.cpp b/voip/jni/rtp/RtpStream.cpp
index 6540099..bfe8e24 100644
--- a/voip/jni/rtp/RtpStream.cpp
+++ b/voip/jni/rtp/RtpStream.cpp
@@ -33,11 +33,11 @@
namespace {
-jfieldID gNative;
+jfieldID gSocket;
jint create(JNIEnv *env, jobject thiz, jstring jAddress)
{
- env->SetIntField(thiz, gNative, -1);
+ env->SetIntField(thiz, gSocket, -1);
sockaddr_storage ss;
if (parse(env, jAddress, 0, &ss) < 0) {
@@ -58,7 +58,7 @@
&((sockaddr_in *)&ss)->sin_port : &((sockaddr_in6 *)&ss)->sin6_port;
uint16_t port = ntohs(*p);
if ((port & 1) == 0) {
- env->SetIntField(thiz, gNative, socket);
+ env->SetIntField(thiz, gSocket, socket);
return port;
}
::close(socket);
@@ -75,7 +75,7 @@
*p = htons(port);
if (bind(socket, (sockaddr *)&ss, sizeof(ss)) == 0) {
- env->SetIntField(thiz, gNative, socket);
+ env->SetIntField(thiz, gSocket, socket);
return port;
}
}
@@ -86,25 +86,15 @@
return -1;
}
-jint dup(JNIEnv *env, jobject thiz)
-{
- int socket = ::dup(env->GetIntField(thiz, gNative));
- if (socket == -1) {
- jniThrowException(env, "java/lang/IllegalStateException", strerror(errno));
- }
- return socket;
-}
-
void close(JNIEnv *env, jobject thiz)
{
- int socket = env->GetIntField(thiz, gNative);
+ int socket = env->GetIntField(thiz, gSocket);
::close(socket);
- env->SetIntField(thiz, gNative, -1);
+ env->SetIntField(thiz, gSocket, -1);
}
JNINativeMethod gMethods[] = {
{"create", "(Ljava/lang/String;)I", (void *)create},
- {"dup", "()I", (void *)dup},
{"close", "()V", (void *)close},
};
@@ -114,7 +104,7 @@
{
jclass clazz;
if ((clazz = env->FindClass("android/net/rtp/RtpStream")) == NULL ||
- (gNative = env->GetFieldID(clazz, "mNative", "I")) == NULL ||
+ (gSocket = env->GetFieldID(clazz, "mSocket", "I")) == NULL ||
env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
ALOGE("JNI registration failed");
return -1;
diff --git a/wifi/java/android/net/wifi/WifiConfigStore.java b/wifi/java/android/net/wifi/WifiConfigStore.java
index a9dbd10..3c761c8 100644
--- a/wifi/java/android/net/wifi/WifiConfigStore.java
+++ b/wifi/java/android/net/wifi/WifiConfigStore.java
@@ -1141,7 +1141,15 @@
String varName = field.varName();
String value = field.value();
if (value != null) {
- if (field != config.eap && field != config.engine) {
+ if (field == config.engine) {
+ /*
+ * If the field is declared as an integer, it must not
+ * be null
+ */
+ if (value.length() == 0) {
+ value = "0";
+ }
+ } else if (field != config.eap) {
value = (value.length() == 0) ? "NULL" : convertToQuotedString(value);
}
if (!mWifiNative.setNetworkVariable(
diff --git a/wifi/java/android/net/wifi/WifiMonitor.java b/wifi/java/android/net/wifi/WifiMonitor.java
index f07e0de..03d5134 100644
--- a/wifi/java/android/net/wifi/WifiMonitor.java
+++ b/wifi/java/android/net/wifi/WifiMonitor.java
@@ -166,6 +166,9 @@
/* P2P-DEVICE-LOST p2p_dev_addr=42:fc:89:e1:e2:27 */
private static final String P2P_DEVICE_LOST_STR = "P2P-DEVICE-LOST";
+ /* P2P-FIND-STOPPED */
+ private static final String P2P_FIND_STOPPED_STR = "P2P-FIND-STOPPED";
+
/* P2P-GO-NEG-REQUEST 42:fc:89:a8:96:09 dev_passwd_id=4 */
private static final String P2P_GO_NEG_REQUEST_STR = "P2P-GO-NEG-REQUEST";
@@ -264,6 +267,7 @@
public static final int P2P_PROV_DISC_PBC_RSP_EVENT = BASE + 34;
public static final int P2P_PROV_DISC_ENTER_PIN_EVENT = BASE + 35;
public static final int P2P_PROV_DISC_SHOW_PIN_EVENT = BASE + 36;
+ public static final int P2P_FIND_STOPPED_EVENT = BASE + 37;
/* hostap events */
public static final int AP_STA_DISCONNECTED_EVENT = BASE + 41;
@@ -516,6 +520,8 @@
mStateMachine.sendMessage(P2P_DEVICE_FOUND_EVENT, new WifiP2pDevice(dataString));
} else if (dataString.startsWith(P2P_DEVICE_LOST_STR)) {
mStateMachine.sendMessage(P2P_DEVICE_LOST_EVENT, new WifiP2pDevice(dataString));
+ } else if (dataString.startsWith(P2P_FIND_STOPPED_STR)) {
+ mStateMachine.sendMessage(P2P_FIND_STOPPED_EVENT);
} else if (dataString.startsWith(P2P_GO_NEG_REQUEST_STR)) {
mStateMachine.sendMessage(P2P_GO_NEGOTIATION_REQUEST_EVENT,
new WifiP2pConfig(dataString));
diff --git a/wifi/java/android/net/wifi/p2p/WifiP2pManager.java b/wifi/java/android/net/wifi/p2p/WifiP2pManager.java
index 4fd0a57..2fc6c20 100644
--- a/wifi/java/android/net/wifi/p2p/WifiP2pManager.java
+++ b/wifi/java/android/net/wifi/p2p/WifiP2pManager.java
@@ -182,6 +182,41 @@
"android.net.wifi.p2p.PEERS_CHANGED";
/**
+ * Broadcast intent action indicating that peer discovery has either started or stopped.
+ * One extra {@link #EXTRA_DISCOVERY_STATE} indicates whether discovery has started
+ * or stopped.
+ *
+ * Note that discovery will be stopped during a connection setup. If the application tries
+ * to re-initiate discovery during this time, it can fail.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String WIFI_P2P_DISCOVERY_CHANGED_ACTION =
+ "android.net.wifi.p2p.DISCOVERY_STATE_CHANGE";
+
+ /**
+ * The lookup key for an int that indicates whether p2p discovery has started or stopped.
+ * Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}.
+ *
+ * @see #WIFI_P2P_DISCOVERY_STARTED
+ * @see #WIFI_P2P_DISCOVERY_STOPPED
+ */
+ public static final String EXTRA_DISCOVERY_STATE = "discoveryState";
+
+ /**
+ * p2p discovery has stopped
+ *
+ * @see #WIFI_P2P_DISCOVERY_CHANGED_ACTION
+ */
+ public static final int WIFI_P2P_DISCOVERY_STOPPED = 1;
+
+ /**
+ * p2p discovery has started
+ *
+ * @see #WIFI_P2P_DISCOVERY_CHANGED_ACTION
+ */
+ public static final int WIFI_P2P_DISCOVERY_STARTED = 2;
+
+ /**
* Broadcast intent action indicating that this device details have changed.
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
diff --git a/wifi/java/android/net/wifi/p2p/WifiP2pService.java b/wifi/java/android/net/wifi/p2p/WifiP2pService.java
index 02ca926..3d3a746 100644
--- a/wifi/java/android/net/wifi/p2p/WifiP2pService.java
+++ b/wifi/java/android/net/wifi/p2p/WifiP2pService.java
@@ -140,6 +140,11 @@
* is invoked */
private boolean mPersistGroup;
+ /* Track whether we are in p2p discovery. This is used to avoid sending duplicate
+ * broadcasts
+ */
+ private boolean mDiscoveryStarted;
+
private NetworkInfo mNetworkInfo;
/* Is chosen as a unique range to avoid conflict with
@@ -489,11 +494,15 @@
case WifiP2pManager.DISCOVER_PEERS:
if (mWifiNative.p2pFind(DISCOVER_TIMEOUT_S)) {
replyToMessage(message, WifiP2pManager.DISCOVER_PEERS_SUCCEEDED);
+ sendP2pDiscoveryChangedBroadcast(true);
} else {
replyToMessage(message, WifiP2pManager.DISCOVER_PEERS_FAILED,
WifiP2pManager.ERROR);
}
break;
+ case WifiMonitor.P2P_FIND_STOPPED_EVENT:
+ sendP2pDiscoveryChangedBroadcast(false);
+ break;
case WifiP2pManager.STOP_DISCOVERY:
if (mWifiNative.p2pStopFind()) {
replyToMessage(message, WifiP2pManager.STOP_DISCOVERY_SUCCEEDED);
@@ -1030,6 +1039,20 @@
mContext.sendStickyBroadcast(intent);
}
+ private void sendP2pDiscoveryChangedBroadcast(boolean started) {
+ if (mDiscoveryStarted == started) return;
+ mDiscoveryStarted = started;
+
+ if (DBG) logd("discovery change broadcast " + started);
+
+ final Intent intent = new Intent(WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.putExtra(WifiP2pManager.EXTRA_DISCOVERY_STATE, started ?
+ WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED :
+ WifiP2pManager.WIFI_P2P_DISCOVERY_STOPPED);
+ mContext.sendStickyBroadcast(intent);
+ }
+
private void sendThisDeviceChangedBroadcast() {
final Intent intent = new Intent(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);