Merge "Put back the status bar animations."
diff --git a/Android.mk b/Android.mk
index ea8314c..752a5f8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -183,6 +183,7 @@
 	media/java/android/media/IAudioFocusDispatcher.aidl \
 	media/java/android/media/IMediaScannerListener.aidl \
 	media/java/android/media/IMediaScannerService.aidl \
+	media/java/android/media/IRemoteControlClient.aidl \
 	telephony/java/com/android/internal/telephony/IPhoneStateListener.aidl \
 	telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl \
 	telephony/java/com/android/internal/telephony/ITelephony.aidl \
diff --git a/api/current.txt b/api/current.txt
index 817ed9c..876d555 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -15,6 +15,7 @@
     field public static final java.lang.String ACCESS_SURFACE_FLINGER = "android.permission.ACCESS_SURFACE_FLINGER";
     field public static final java.lang.String ACCESS_WIFI_STATE = "android.permission.ACCESS_WIFI_STATE";
     field public static final java.lang.String ACCOUNT_MANAGER = "android.permission.ACCOUNT_MANAGER";
+    field public static final java.lang.String ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL";
     field public static final java.lang.String AUTHENTICATE_ACCOUNTS = "android.permission.AUTHENTICATE_ACCOUNTS";
     field public static final java.lang.String BATTERY_STATS = "android.permission.BATTERY_STATS";
     field public static final java.lang.String BIND_APPWIDGET = "android.permission.BIND_APPWIDGET";
@@ -24134,7 +24135,7 @@
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator CREATOR;
     field public static final int RESULT_ATTR_IN_THE_DICTIONARY = 1; // 0x1
-    field public static final int RESULT_ATTR_LOOKS_TYPO = 2; // 0x2
+    field public static final int RESULT_ATTR_LOOKS_LIKE_TYPO = 2; // 0x2
   }
 
   public final class TextInfo implements android.os.Parcelable {
diff --git a/core/java/android/server/BluetoothPanProfileHandler.java b/core/java/android/server/BluetoothPanProfileHandler.java
index 0d63e19..ff22a45 100644
--- a/core/java/android/server/BluetoothPanProfileHandler.java
+++ b/core/java/android/server/BluetoothPanProfileHandler.java
@@ -173,7 +173,7 @@
 
                 if (!mBluetoothService.disconnectPanServerDeviceNative(objectPath,
                         device.getAddress(),
-                        panDevice.mIfaceAddr)) {
+                        panDevice.mIface)) {
                     errorLog("could not disconnect Pan Server Device "+device.getAddress());
 
                     // Restore prev state
@@ -291,6 +291,7 @@
             panDevice.mState = state;
             panDevice.mIfaceAddr = ifaceAddr;
             panDevice.mLocalRole = role;
+            panDevice.mIface = iface;
         }
 
         Intent intent = new Intent(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java
index 66d2641..744d564 100644
--- a/core/java/android/view/HardwareRenderer.java
+++ b/core/java/android/view/HardwareRenderer.java
@@ -713,17 +713,23 @@
             // Cancels any existing buffer to ensure we'll get a buffer
             // of the right size before we call eglSwapBuffers
             sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
-            sEgl.eglDestroySurface(sEglDisplay, mEglSurface);
-            mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, holder, null);
+            
+            if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) {
+                sEgl.eglDestroySurface(sEglDisplay, mEglSurface);
+            }
 
-            if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) {
-                int error = sEgl.eglGetError();
-                if (error == EGL_BAD_NATIVE_WINDOW) {
-                    Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
-                    return;
+            if (holder.getSurface().isValid()) {
+                mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, holder, null);
+    
+                if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) {
+                    int error = sEgl.eglGetError();
+                    if (error == EGL_BAD_NATIVE_WINDOW) {
+                        Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+                        return;
+                    }
+                    throw new RuntimeException("createWindowSurface failed "
+                            + getEGLErrorString(error));
                 }
-                throw new RuntimeException("createWindowSurface failed "
-                        + getEGLErrorString(error));
             }
         }
 
diff --git a/core/java/android/view/textservice/SuggestionsInfo.java b/core/java/android/view/textservice/SuggestionsInfo.java
index 3332f1e..ed0f89d 100644
--- a/core/java/android/view/textservice/SuggestionsInfo.java
+++ b/core/java/android/view/textservice/SuggestionsInfo.java
@@ -34,9 +34,9 @@
     /**
      * Flag of the attributes of the suggestions that can be obtained by
      * {@link #getSuggestionsAttributes}: this tells that the text service thinks the requested
-     * word looks a typo.
+     * word looks like a typo.
      */
-    public static final int RESULT_ATTR_LOOKS_TYPO = 0x0002;
+    public static final int RESULT_ATTR_LOOKS_LIKE_TYPO = 0x0002;
     private final int mSuggestionsAttributes;
     private final String[] mSuggestions;
     private final boolean mSuggestionsAvailable;
diff --git a/core/java/android/webkit/DeviceMotionService.java b/core/java/android/webkit/DeviceMotionService.java
index 7d7a0f0..b4d5759 100755
--- a/core/java/android/webkit/DeviceMotionService.java
+++ b/core/java/android/webkit/DeviceMotionService.java
@@ -99,6 +99,7 @@
         mUpdateRunnable = new Runnable() {
             @Override
             public void run() {
+                assert mIsRunning;
                 mManager.onMotionChange(new Double(mLastAcceleration[0]),
                         new Double(mLastAcceleration[1]), new Double(mLastAcceleration[2]),
                         INTERVAL_MILLIS);
@@ -157,6 +158,11 @@
         assert WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName());
         assert(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER);
 
+        // We may get callbacks after the call to getSensorManager().unregisterListener() returns.
+        if (!mIsRunning) {
+            return;
+        }
+
         boolean firstData = mLastAcceleration == null;
         mLastAcceleration = event.values;
         if (firstData) {
diff --git a/core/java/android/webkit/DeviceOrientationService.java b/core/java/android/webkit/DeviceOrientationService.java
index f3c0576..47c8ab7 100755
--- a/core/java/android/webkit/DeviceOrientationService.java
+++ b/core/java/android/webkit/DeviceOrientationService.java
@@ -188,6 +188,7 @@
         assert(event.values.length == 3);
         assert WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName());
 
+        // We may get callbacks after the call to getSensorManager().unregisterListener() returns.
         if (!mIsRunning) {
             return;
         }
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index f4fd551..7620a63 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -8405,9 +8405,9 @@
         }
     }
 
-    // Called by JNI to invalidate the View, given rectangle coordinates in
-    // content space
-    private void pageSwapCallback() {
+    /** @hide Called by JNI when pages are swapped (only occurs with hardware
+     * acceleration) */
+    protected void pageSwapCallback() {
         if (inEditingMode()) {
             didUpdateWebTextViewDimensions(ANYWHERE);
         }
@@ -8426,11 +8426,11 @@
         WebViewCore.ViewState viewState = draw.mViewState;
         boolean isPictureAfterFirstLayout = viewState != null;
 
-        // Request a callback on pageSwap (to reposition the webtextview)
-        boolean registerPageSwapCallback =
-            !mZoomManager.isFixedLengthAnimationInProgress() && inEditingMode();
-
         if (updateBaseLayer) {
+            // Request a callback on pageSwap (to reposition the webtextview)
+            boolean registerPageSwapCallback =
+                !mZoomManager.isFixedLengthAnimationInProgress() && inEditingMode();
+
             setBaseLayer(draw.mBaseLayer, draw.mInvalRegion,
                     getSettings().getShowVisualIndicator(),
                     isPictureAfterFirstLayout, registerPageSwapCallback);
@@ -9084,6 +9084,16 @@
         }
     }
 
+    /** @hide send content invalidate */
+    protected void contentInvalidateAll() {
+        mWebViewCore.sendMessage(EventHub.CONTENT_INVALIDATE_ALL);
+    }
+
+    /** @hide call pageSwapCallback upon next page swap */
+    protected void registerPageSwapCallback() {
+        nativeRegisterPageSwapCallback();
+    }
+
     /**
      * Begin collecting per-tile profiling data
      *
@@ -9245,6 +9255,7 @@
     private native void     nativeStopGL();
     private native Rect     nativeSubtractLayers(Rect content);
     private native int      nativeTextGeneration();
+    private native void     nativeRegisterPageSwapCallback();
     private native void     nativeTileProfilingStart();
     private native float    nativeTileProfilingStop();
     private native void     nativeTileProfilingClear();
diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java
index 8d8023b..400cdbd 100644
--- a/core/java/android/webkit/WebViewCore.java
+++ b/core/java/android/webkit/WebViewCore.java
@@ -999,6 +999,7 @@
         static final int DUMP_V8COUNTERS = 173;
 
         static final int SET_JS_FLAGS = 174;
+        static final int CONTENT_INVALIDATE_ALL = 175;
         // Geolocation
         static final int GEOLOCATION_PERMISSIONS_PROVIDE = 180;
 
@@ -1503,6 +1504,10 @@
                             nativeSetJsFlags((String)msg.obj);
                             break;
 
+                        case CONTENT_INVALIDATE_ALL:
+                            nativeContentInvalidateAll();
+                            break;
+
                         case SAVE_WEBARCHIVE:
                             WebView.SaveWebArchiveMessage saveMessage =
                                 (WebView.SaveWebArchiveMessage)msg.obj;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 65ee745..9de94cf 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -343,6 +343,9 @@
     private Drawable mSelectHandleRight;
     private Drawable mSelectHandleCenter;
 
+    // Global listener that detects changes in the global position of the TextView
+    private PositionListener mPositionListener;
+
     private float mLastDownPositionX, mLastDownPositionY;
     private Callback mCustomSelectionActionModeCallback;
 
@@ -394,7 +397,7 @@
          */
         boolean onEditorAction(TextView v, int actionId, KeyEvent event);
     }
-    
+
     public TextView(Context context) {
         this(context, null);
     }
@@ -2081,7 +2084,7 @@
                                        TextAppearance_textStyle, -1);
 
         setTypefaceByIndex(typefaceIndex, styleIndex);
-        
+
         if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps,
                 false)) {
             setTransformationMethod(new AllCapsTransformationMethod(getContext()));
@@ -3019,7 +3022,7 @@
      * To style your strings, attach android.text.style.* objects to a
      * {@link android.text.SpannableString SpannableString}, or see the
      * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
-     * Available Resource Types</a> documentation for an example of setting 
+     * Available Resource Types</a> documentation for an example of setting
      * formatted text in the XML resource file.
      *
      * @attr ref android.R.styleable#TextView_text
@@ -8757,32 +8760,247 @@
         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 modified);
+    }
+
+    private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
+        // 3 handles, 2 ActionPopup (suggestionsPopup first hides the others)
+        private final int MAXIMUM_NUMBER_OF_LISTENERS = 5;
+        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;
+
+        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 || mCanMove[i]) {
+                    TextViewPositionListener positionListener = mPositionListeners[i];
+                    if (positionListener != null) {
+                        positionListener.updatePosition(mPositionX, mPositionY,
+                                mPositionHasChanged);
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        private void updatePosition() {
+            TextView.this.getLocationInWindow(mTempCoords);
+
+            mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
+
+            mPositionX = mTempCoords[0];
+            mPositionY = mTempCoords[1];
+        }
+
+        public boolean isVisible(int positionX, int positionY) {
+            final TextView textView = TextView.this;
+
+            if (mTempRect == null) mTempRect = new Rect();
+            final Rect clip = mTempRect;
+            clip.left = getCompoundPaddingLeft();
+            clip.top = getExtendedPaddingTop();
+            clip.right = textView.getWidth() - getCompoundPaddingRight();
+            clip.bottom = textView.getHeight() - getExtendedPaddingBottom();
+
+            final ViewParent parent = textView.getParent();
+            if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
+                return false;
+            }
+
+            int posX = mPositionX + positionX;
+            int posY = mPositionY + positionY;
+
+            // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
+            return posX >= clip.left - 1 && posX <= clip.right + 1 &&
+                    posY >= clip.top && posY <= clip.bottom;
+        }
+
+        public boolean isOffsetVisible(int offset) {
+            final int line = mLayout.getLineForOffset(offset);
+            final int lineBottom = mLayout.getLineBottom(line);
+            final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
+            return isVisible(primaryHorizontal, lineBottom);
+        }
+    }
+
+    private abstract class PinnedPopupWindow implements TextViewPositionListener {
+        protected PopupWindow mPopupWindow;
+        protected LinearLayout 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);
+
+            mContentView = new LinearLayout(TextView.this.getContext());
+            LayoutParams wrapContent = new LayoutParams(
+                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+            mContentView.setLayoutParams(wrapContent);
+
+            initContentView();
+            mPopupWindow.setContentView(mContentView);
+        }
+
+        public void show() {
+            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));
+
+            TextView.this.getPositionListener().addSubscriber(this, false);
+
+            computeLocalPosition();
+
+            final PositionListener positionListener = TextView.this.getPositionListener();
+            updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
+        }
+
+        private void computeLocalPosition() {
+            final int offset = getTextOffset();
+
+            final int width = mContentView.getMeasuredWidth();
+            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 modified) {
+            if (isShowing() && getPositionListener().isOffsetVisible(getTextOffset())) {
+                updatePosition(parentPositionX, parentPositionY);
+            } else {
+                hide();
+            }
+        }
+
+        public boolean isShowing() {
+            return mPopupWindow.isShowing();
+        }
+    }
+
     private static class SuggestionRangeSpan extends UnderlineSpan {
         // TODO themable, would be nice to make it a child class of TextAppearanceSpan, but
         // there is no way to have underline and TextAppearanceSpan.
     }
 
-    private class SuggestionsPopupWindow implements OnClickListener {
+    private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnClickListener {
         private static final int MAX_NUMBER_SUGGESTIONS = 5;
         private static final int NO_SUGGESTIONS = -1;
-        private final PopupWindow mPopupWindow;
-        private LinearLayout mSuggestionsContainer;
         private WordIterator mSuggestionWordIterator;
         private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0];
 
-        public SuggestionsPopupWindow() {
+        @Override
+        protected void createPopupWindow() {
             mPopupWindow = new PopupWindow(TextView.this.mContext, null,
-                    com.android.internal.R.attr.textSuggestionsWindowStyle);
-            mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+                com.android.internal.R.attr.textSuggestionsWindowStyle);
             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
             mPopupWindow.setOutsideTouchable(true);
-            mPopupWindow.setClippingEnabled(true);
+            mPopupWindow.setClippingEnabled(false);
+        }
 
-            mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
-            mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
-
-            mSuggestionsContainer = new LinearLayout(TextView.this.mContext);
-            mSuggestionsContainer.setOrientation(LinearLayout.VERTICAL);
+        @Override
+        protected void initContentView() {
+            mContentView.setOrientation(LinearLayout.VERTICAL);
 
             LayoutInflater inflater = (LayoutInflater) TextView.this.mContext.
                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -8795,7 +9013,7 @@
             // Inflate the suggestion items once and for all.
             for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
                 View childView = inflater.inflate(mTextEditSuggestionItemLayout,
-                        mSuggestionsContainer, false);
+                        mContentView, false);
 
                 if (! (childView instanceof TextView)) {
                     throw new IllegalArgumentException(
@@ -8803,11 +9021,9 @@
                 }
 
                 childView.setTag(new SuggestionInfo());
-                mSuggestionsContainer.addView(childView);
+                mContentView.addView(childView);
                 childView.setOnClickListener(this);
             }
-
-            mPopupWindow.setContentView(mSuggestionsContainer);
         }
 
         private class SuggestionInfo {
@@ -8827,30 +9043,61 @@
             SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
 
             // Cache the span length for performance reason.
-            final HashMap<SuggestionSpan, Integer> spanLengthMap =
-                new HashMap<SuggestionSpan, Integer>();
+            final HashMap<SuggestionSpan, Integer> spansLengths =
+                    new HashMap<SuggestionSpan, Integer>();
 
             for (SuggestionSpan suggestionSpan : suggestionSpans) {
                 int start = spannable.getSpanStart(suggestionSpan);
                 int end = spannable.getSpanEnd(suggestionSpan);
-                spanLengthMap.put(suggestionSpan, end - start);
+                spansLengths.put(suggestionSpan, Integer.valueOf(end - start));
             }
 
             // The suggestions are sorted according to the lenght of the text that they cover
             // (shorter first)
             Arrays.sort(suggestionSpans, new Comparator<SuggestionSpan>() {
                 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
-                    return spanLengthMap.get(span1) - spanLengthMap.get(span2);
+                    return spansLengths.get(span1).intValue() - spansLengths.get(span2).intValue();
                 }
             });
 
             return suggestionSpans;
         }
 
+        @Override
         public void show() {
             if (!(mText instanceof Editable)) return;
+            updateSuggestions();
 
-            Spannable spannable = (Spannable) TextView.this.mText;
+            super.show();
+        }
+
+        @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();
+            if ((mText instanceof Editable) && mSuggestionRangeSpan != null) {
+                ((Editable) mText).removeSpan(mSuggestionRangeSpan);
+            }
+        }
+
+        private void updateSuggestions() {
+            Spannable spannable = (Spannable)TextView.this.mText;
             SuggestionSpan[] suggestionSpans = getSuggestionSpans();
 
             final int nbSpans = suggestionSpans.length;
@@ -8869,7 +9116,7 @@
                 String[] suggestions = suggestionSpan.getSuggestions();
                 int nbSuggestions = suggestions.length;
                 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
-                    TextView textView = (TextView) mSuggestionsContainer.getChildAt(
+                    TextView textView = (TextView) mContentView.getChildAt(
                             totalNbSuggestions);
                     textView.setText(suggestions[suggestionIndex]);
                     SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
@@ -8889,7 +9136,7 @@
 
             if (totalNbSuggestions == 0) {
                 // TODO Replace by final text, use a dedicated layout, add a fade out timer...
-                TextView textView = (TextView) mSuggestionsContainer.getChildAt(0);
+                TextView textView = (TextView) mContentView.getChildAt(0);
                 textView.setText("No suggestions available");
                 SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
                 suggestionInfo.spanStart = NO_SUGGESTIONS;
@@ -8900,26 +9147,17 @@
                         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
 
                 for (int i = 0; i < totalNbSuggestions; i++) {
-                    final TextView textView = (TextView) mSuggestionsContainer.getChildAt(i);
+                    final TextView textView = (TextView) mContentView.getChildAt(i);
                     highlightTextDifferences(textView, spanUnionStart, spanUnionEnd);
                 }
             }
 
             for (int i = 0; i < totalNbSuggestions; i++) {
-                mSuggestionsContainer.getChildAt(i).setVisibility(VISIBLE);
+                mContentView.getChildAt(i).setVisibility(VISIBLE);
             }
             for (int i = totalNbSuggestions; i < MAX_NUMBER_SUGGESTIONS; i++) {
-                mSuggestionsContainer.getChildAt(i).setVisibility(GONE);
+                mContentView.getChildAt(i).setVisibility(GONE);
             }
-
-            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
-            final int screenWidth = displayMetrics.widthPixels;
-            final int screenHeight = displayMetrics.heightPixels;
-            mSuggestionsContainer.measure(
-                    View.MeasureSpec.makeMeasureSpec(screenWidth, View.MeasureSpec.AT_MOST),
-                    View.MeasureSpec.makeMeasureSpec(screenHeight, View.MeasureSpec.AT_MOST));
-
-            positionAtCursor();
         }
 
         private long[] getWordLimits(CharSequence text) {
@@ -9071,17 +9309,6 @@
             textView.setText(ssb);
         }
 
-        public void hide() {
-            if ((mText instanceof Editable) && mSuggestionRangeSpan != null) {
-                ((Editable) mText).removeSpan(mSuggestionRangeSpan);
-            }
-            mPopupWindow.dismiss();
-        }
-
-        public boolean isShowing() {
-            return mPopupWindow.isShowing();
-        }
-
         @Override
         public void onClick(View view) {
             if (view instanceof TextView) {
@@ -9141,44 +9368,6 @@
             }
             hide();
         }
-
-        void positionAtCursor() {
-            View contentView = mPopupWindow.getContentView();
-            int width = contentView.getMeasuredWidth();
-            int height = contentView.getMeasuredHeight();
-            final int offset = TextView.this.getSelectionStart();
-            final int line = mLayout.getLineForOffset(offset);
-            final int lineBottom = mLayout.getLineBottom(line);
-            float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
-
-            final Rect bounds = sCursorControllerTempRect;
-            bounds.left = (int) (primaryHorizontal - width / 2.0f);
-            bounds.top = lineBottom;
-
-            bounds.right = bounds.left + width;
-            bounds.bottom = bounds.top + height;
-
-            convertFromViewportToContentCoordinates(bounds);
-
-            final int[] coords = mTempCoords;
-            TextView.this.getLocationInWindow(coords);
-            coords[0] += bounds.left;
-            coords[1] += bounds.top;
-
-            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
-            final int screenHeight = displayMetrics.heightPixels;
-
-            // Vertical clipping
-            if (coords[1] + height > screenHeight) {
-                coords[1] = screenHeight - height;
-            }
-
-            // Horizontal clipping
-            coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]);
-            coords[0] = Math.max(0, coords[0]);
-
-            mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
-        }
     }
 
     void showSuggestions() {
@@ -9359,7 +9548,7 @@
             boolean allowText = getContext().getResources().getBoolean(
                     com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
 
-            mode.setTitle(allowText ? 
+            mode.setTitle(allowText ?
                     mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
             mode.setSubtitle(null);
 
@@ -9452,29 +9641,23 @@
         }
     }
 
-    private class ActionPopupWindow implements OnClickListener {
-        private static final int TEXT_EDIT_ACTION_POPUP_TEXT =
+    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 final PopupWindow mPopupWindow;
         private TextView mPasteTextView;
         private TextView mReplaceTextView;
-        private LinearLayout mContentView;
         // Whether or not the Paste action should be available when the action popup is displayed
         private boolean mWithPaste;
 
-        public ActionPopupWindow() {
+        @Override
+        protected void createPopupWindow() {
             mPopupWindow = new PopupWindow(TextView.this.mContext, null,
                     com.android.internal.R.attr.textSelectHandleWindowStyle);
             mPopupWindow.setClippingEnabled(true);
-            mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL);
+        }
 
-            mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
-            mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
-
-            mContentView = new LinearLayout(TextView.this.getContext());
-            LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
-                    ViewGroup.LayoutParams.WRAP_CONTENT);
-            mContentView.setLayoutParams(wrapContent);
+        @Override
+        protected void initContentView() {
             mContentView.setOrientation(LinearLayout.HORIZONTAL);
             mContentView.setBackgroundResource(
                     com.android.internal.R.drawable.text_edit_side_paste_window);
@@ -9482,36 +9665,26 @@
             LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 
-            mPasteTextView = (TextView) inflater.inflate(TEXT_EDIT_ACTION_POPUP_TEXT, null);
+            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(TEXT_EDIT_ACTION_POPUP_TEXT, null);
+            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);
-
-            mPopupWindow.setContentView(mContentView);
         }
 
+        @Override
         public void show() {
             mPasteTextView.setVisibility(mWithPaste && canPaste() ? View.VISIBLE : View.GONE);
-
-            final int size = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
-            mContentView.measure(size, size);
-
-            positionAtCursor();
-        }
-
-        public void hide() {
-            mPopupWindow.dismiss();
-        }
-
-        public boolean isShowing() {
-            return mPopupWindow.isShowing();
+            super.show();
         }
 
         @Override
@@ -9524,48 +9697,30 @@
             }
         }
 
-        void positionAtCursor() {
-            int width = mContentView.getMeasuredWidth();
-            int height = mContentView.getMeasuredHeight();
-            final int selectionStart = TextView.this.getSelectionStart();
-            final int selectionEnd = TextView.this.getSelectionEnd();
-            final int offset = (selectionStart + selectionEnd) / 2;
-            final int line = mLayout.getLineForOffset(offset);
-            final int lineTop = mLayout.getLineTop(line);
-            float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
+        @Override
+        protected int getTextOffset() {
+            return (getSelectionStart() + getSelectionEnd()) / 2;
+        }
 
-            final Rect bounds = sCursorControllerTempRect;
-            bounds.left = (int) (primaryHorizontal - width / 2.0f);
-            bounds.top = lineTop - height;
+        @Override
+        protected int getVerticalLocalPosition(int line) {
+            return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
+        }
 
-            bounds.right = bounds.left + width;
-            bounds.bottom = bounds.top + height;
-
-            convertFromViewportToContentCoordinates(bounds);
-
-            final int[] coords = mTempCoords;
-            TextView.this.getLocationInWindow(coords);
-            coords[0] += bounds.left;
-            coords[1] += bounds.top;
-
-            // Vertical clipping, move under edited line and to the side of insertion cursor
-            if (coords[1] < 0) {
-                coords[1] += height;
-                final int lineBottom = mLayout.getLineBottom(line);
-                final int lineHeight = lineBottom - lineTop;
-                coords[1] += lineHeight;
+        @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);
-                coords[1] += handle.getIntrinsicHeight();
+                positionY += handle.getIntrinsicHeight();
             }
 
-            // Horizontal clipping
-            coords[0] = Math.max(0, coords[0]);
-            final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
-            coords[0] = Math.min(screenWidth - width, coords[0]);
-
-            mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
+            return positionY;
         }
 
         public void setShowWithPaste(boolean withPaste) {
@@ -9573,7 +9728,7 @@
         }
     }
 
-    private abstract class HandleView extends View implements ViewTreeObserver.OnPreDrawListener {
+    private abstract class HandleView extends View implements TextViewPositionListener {
         protected Drawable mDrawable;
         private final PopupWindow mContainer;
         // Position with respect to the parent TextView
@@ -9581,21 +9736,19 @@
         private boolean mIsDragging;
         // Offset from touch position to mPosition
         private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
-        protected float mHotspotX;
+        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;
-        // PopupWindow container absolute position with respect to the enclosing window
-        private int mContainerPositionX, mContainerPositionY;
-        // Visible or not (scrolled off screen), whether or not this handle should be visible
-        private boolean mIsActive = false;
-        // Used to detect that setFrame was called
-        private boolean mNeedsUpdate = true;
         // 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;
 
@@ -9615,15 +9768,6 @@
             mIdealVerticalOffset = 0.7f * handleHeight;
         }
 
-        @Override
-        protected boolean setFrame(int left, int top, int right, int bottom) {
-            boolean changed = super.setFrame(left, top, right, bottom);
-            // onPreDraw is called for PhoneWindow before the layout of this view is
-            // performed. Make sure to update position, even if container didn't move.
-            if (changed) mNeedsUpdate  = true;
-            return changed;
-        }
-
         protected abstract void initDrawable();
 
         // Touch-up filter: number of previous positions remembered
@@ -9641,12 +9785,6 @@
         }
 
         private void addPositionToTouchUpFilter(int offset) {
-            if (mNumberPreviousOffsets > 0 &&
-                    mPreviousOffsets[mPreviousOffsetIndex] == offset) {
-                // Make sure only actual changes of position are recorded.
-                return;
-            }
-
             mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
             mPreviousOffsets[mPreviousOffsetIndex] = offset;
             mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
@@ -9665,7 +9803,7 @@
 
             if (i > 0 && i < iMax &&
                     (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
-                updateOffset(mPreviousOffsets[index]);
+                positionAtCursorOffset(mPreviousOffsets[index]);
             }
         }
 
@@ -9675,18 +9813,14 @@
         }
 
         public void show() {
-            if (isShowing()) {
-                mContainer.update(mContainerPositionX, mContainerPositionY, -1, -1);
-            } else {
-                mContainer.showAtLocation(TextView.this, 0,
-                        mContainerPositionX, mContainerPositionY);
+            if (isShowing()) return;
 
-                if (!mIsActive) {
-                    ViewTreeObserver vto = TextView.this.getViewTreeObserver();
-                    vto.addOnPreDrawListener(this);
-                    mIsActive = true;
-                }
-            }
+            getPositionListener().addSubscriber(this, true);
+
+            // Make sure the offset is always considered new, even when focusing at same position
+            mPreviousOffset = -1;
+            positionAtCursorOffset(getCurrentCursorOffset());
+
             hideActionPopupWindow();
         }
 
@@ -9699,9 +9833,7 @@
         public void hide() {
             dismiss();
 
-            ViewTreeObserver vto = TextView.this.getViewTreeObserver();
-            vto.removeOnPreDrawListener(this);
-            mIsActive = false;
+            TextView.this.getPositionListener().removeSubscriber(this);
         }
 
         void showActionPopupWindow(int delay, boolean withPaste) {
@@ -9734,7 +9866,7 @@
             return mContainer.isShowing();
         }
 
-        private boolean isPositionVisible() {
+        private boolean isVisible() {
             // Always show a dragging handle.
             if (mIsDragging) {
                 return true;
@@ -9744,103 +9876,71 @@
                 return false;
             }
 
-            final int extendedPaddingTop = getExtendedPaddingTop();
-            final int extendedPaddingBottom = getExtendedPaddingBottom();
-            final int compoundPaddingLeft = getCompoundPaddingLeft();
-            final int compoundPaddingRight = getCompoundPaddingRight();
-
-            final TextView textView = TextView.this;
-
-            if (mTempRect == null) mTempRect = new Rect();
-            final Rect clip = mTempRect;
-            clip.left = compoundPaddingLeft;
-            clip.top = extendedPaddingTop;
-            clip.right = textView.getWidth() - compoundPaddingRight;
-            clip.bottom = textView.getHeight() - extendedPaddingBottom;
-
-            final ViewParent parent = textView.getParent();
-            if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
-                return false;
-            }
-
-            final int[] coords = mTempCoords;
-            textView.getLocationInWindow(coords);
-            final int posX = coords[0] + mPositionX + (int) mHotspotX;
-            final int posY = coords[1] + mPositionY;
-
-            // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
-            return posX >= clip.left - 1 && posX <= clip.right + 1 &&
-                    posY >= clip.top && posY <= clip.bottom;
+            return getPositionListener().isVisible(mPositionX + mHotspotX, mPositionY);
         }
 
         public abstract int getCurrentCursorOffset();
 
-        public abstract void updateOffset(int offset);
+        public abstract void updateSelection(int offset);
 
         public abstract void updatePosition(float x, float y);
 
         protected void positionAtCursorOffset(int offset) {
-            // A HandleView relies on the layout, which may be nulled by external methods.
+            // 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;
             }
 
-            addPositionToTouchUpFilter(offset);
-            final int line = mLayout.getLineForOffset(offset);
-            final int lineBottom = mLayout.getLineBottom(line);
+            if (offset != mPreviousOffset) {
+                updateSelection(offset);
+                addPositionToTouchUpFilter(offset);
+                final int line = mLayout.getLineForOffset(offset);
 
-            mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
-            mPositionY = lineBottom;
+                mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
+                mPositionY = mLayout.getLineBottom(line);
 
-            // Take TextView's padding into account.
-            mPositionX += viewportToContentHorizontalOffset();
-            mPositionY += viewportToContentVerticalOffset();
+                // Take TextView's padding into account.
+                mPositionX += viewportToContentHorizontalOffset();
+                mPositionY += viewportToContentVerticalOffset();
+
+                mPreviousOffset = offset;
+                mPositionHasChanged = true;
+            }
         }
 
-        private void checkForContainerPositionChange() {
-            positionAtCursorOffset(getCurrentCursorOffset());
-
-            final int previousContainerPositionX = mContainerPositionX;
-            final int previousContainerPositionY = mContainerPositionY;
-
-            TextView.this.getLocationInWindow(mTempCoords);
-            mContainerPositionX = mTempCoords[0] + mPositionX;
-            mContainerPositionY = mTempCoords[1] + mPositionY;
-
-            mNeedsUpdate |= previousContainerPositionX != mContainerPositionX;
-            mNeedsUpdate |= previousContainerPositionY != mContainerPositionY;
-        }
-
-        public boolean onPreDraw() {
-            checkForContainerPositionChange();
-            if (mNeedsUpdate) {
+        public void updatePosition(int parentPositionX, int parentPositionY, boolean modified) {
+            if (modified || mPositionHasChanged) {
                 if (mIsDragging) {
-                    if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) {
-                        mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX;
-                        mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY;
-                        mLastParentX = mTempCoords[0];
-                        mLastParentY = mTempCoords[1];
+                    // 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 (isPositionVisible()) {
-                    mContainer.update(mContainerPositionX, mContainerPositionY, -1, -1);
-
-                    if (mIsActive && !isShowing()) {
-                        show();
+                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();
                     }
                 }
-                mNeedsUpdate = false;
+
+                mPositionHasChanged = false;
             }
-            return true;
         }
 
         @Override
@@ -9857,10 +9957,9 @@
                     mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
                     mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
 
-                    final int[] coords = mTempCoords;
-                    TextView.this.getLocationInWindow(coords);
-                    mLastParentX = coords[0];
-                    mLastParentY = coords[1];
+                    final PositionListener positionListener = getPositionListener();
+                    mLastParentX = positionListener.getPositionX();
+                    mLastParentY = positionListener.getPositionY();
                     mIsDragging = true;
                     break;
                 }
@@ -9963,7 +10062,7 @@
                         mTextSelectHandleRes);
             }
             mDrawable = mSelectHandleCenter;
-            mHotspotX = mDrawable.getIntrinsicWidth() / 2.0f;
+            mHotspotX = mDrawable.getIntrinsicWidth() / 2;
         }
 
         @Override
@@ -10008,13 +10107,13 @@
         }
 
         @Override
-        public void updateOffset(int offset) {
+        public void updateSelection(int offset) {
             Selection.setSelection((Spannable) mText, offset);
         }
 
         @Override
         public void updatePosition(float x, float y) {
-            updateOffset(getOffsetForPosition(x, y));
+            positionAtCursorOffset(getOffsetForPosition(x, y));
         }
 
         @Override
@@ -10038,7 +10137,7 @@
                         mTextSelectHandleLeftRes);
             }
             mDrawable = mSelectHandleLeft;
-            mHotspotX = mDrawable.getIntrinsicWidth() * 3.0f / 4.0f;
+            mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4;
         }
 
         @Override
@@ -10047,7 +10146,7 @@
         }
 
         @Override
-        public void updateOffset(int offset) {
+        public void updateSelection(int offset) {
             Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
         }
 
@@ -10063,7 +10162,7 @@
             // Handles can not cross and selection is at least one character
             if (offset >= selectionEnd) offset = selectionEnd - 1;
 
-            Selection.setSelection((Spannable) mText, offset, selectionEnd);
+            positionAtCursorOffset(offset);
         }
 
         public ActionPopupWindow getActionPopupWindow() {
@@ -10079,7 +10178,7 @@
                         mTextSelectHandleRightRes);
             }
             mDrawable = mSelectHandleRight;
-            mHotspotX = mDrawable.getIntrinsicWidth() / 4.0f;
+            mHotspotX = mDrawable.getIntrinsicWidth() / 4;
         }
 
         @Override
@@ -10088,7 +10187,7 @@
         }
 
         @Override
-        public void updateOffset(int offset) {
+        public void updateSelection(int offset) {
             Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
         }
 
@@ -10104,7 +10203,7 @@
             // Handles can not cross and selection is at least one character
             if (offset <= selectionStart) offset = selectionStart + 1;
 
-            Selection.setSelection((Spannable) mText, selectionStart, offset);
+            positionAtCursorOffset(offset);
         }
 
         public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java
index 7839a08..5e70e4c 100644
--- a/core/java/com/android/internal/view/menu/MenuBuilder.java
+++ b/core/java/com/android/internal/view/menu/MenuBuilder.java
@@ -150,6 +150,11 @@
 
     private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
             new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
+
+    /**
+     * Currently expanded menu item; must be collapsed when we clear.
+     */
+    private MenuItemImpl mExpandedItem;
     
     /**
      * Called by menu to notify of close and selection changes.
@@ -512,6 +517,9 @@
     }
     
     public void clear() {
+        if (mExpandedItem != null) {
+            collapseItemActionView(mExpandedItem);
+        }
         mItems.clear();
         
         onItemsChanged(true);
@@ -1223,11 +1231,14 @@
         }
         startDispatchingItemsChanged();
 
+        if (expanded) {
+            mExpandedItem = item;
+        }
         return expanded;
     }
 
     public boolean collapseItemActionView(MenuItemImpl item) {
-        if (mPresenters.isEmpty()) return false;
+        if (mPresenters.isEmpty() || mExpandedItem != item) return false;
 
         boolean collapsed = false;
 
@@ -1242,6 +1253,9 @@
         }
         startDispatchingItemsChanged();
 
+        if (collapsed) {
+            mExpandedItem = null;
+        }
         return collapsed;
     }
 }
diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java
index 446c842..4878b0f 100644
--- a/core/java/com/android/internal/widget/ActionBarView.java
+++ b/core/java/com/android/internal/widget/ActionBarView.java
@@ -1265,9 +1265,8 @@
         @Override
         public void initForMenu(Context context, MenuBuilder menu) {
             // Clear the expanded action view when menus change.
-            mExpandedActionView = null;
-            if (mCurrentExpandedItem != null) {
-                mCurrentExpandedItem.collapseActionView();
+            if (mMenu != null && mCurrentExpandedItem != null) {
+                mMenu.collapseItemActionView(mCurrentExpandedItem);
             }
             mMenu = menu;
         }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f99a94c..21c3f1e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -294,12 +294,21 @@
 
    <!-- Allows an application to read/write the voicemails owned by its own
         package. -->
+   <!--  TODO: delete this permission when dependent content provider &
+        application code has been migrated to use ADD_VOICEMAIL instead -->
     <permission android:name="com.android.voicemail.permission.READ_WRITE_OWN_VOICEMAIL"
         android:permissionGroup="android.permission-group.PERSONAL_INFO"
         android:protectionLevel="dangerous"
         android:label="@string/permlab_readWriteOwnVoicemail"
         android:description="@string/permdesc_readWriteOwnVoicemail" />
 
+   <!-- Allows an application to add voicemails into the system. -->
+    <permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"
+        android:permissionGroup="android.permission-group.PERSONAL_INFO"
+        android:protectionLevel="dangerous"
+        android:label="@string/permlab_addVoicemail"
+        android:description="@string/permdesc_addVoicemail" />
+
     <!-- ======================================= -->
     <!-- Permissions for accessing location info -->
     <!-- ======================================= -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index c5aa4b2..a6c92f2 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2166,6 +2166,14 @@
       voicemails that its associated service can access.</string>
 
     <!-- Title of an application permission, listed so the user can choose whether
+        they want to allow the application to do this. [CHAR LIMIT=NONE] -->
+    <string name="permlab_addVoicemail">add voicemail</string>
+    <!-- Description of an application permission, listed so the user can choose whether
+        they want to allow the application to do this. [CHAR LIMIT=NONE] -->
+    <string name="permdesc_addVoicemail">Allows the application to add messages
+      to your voicemail inbox.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether
         they want to allow the application to do this. -->
     <string name="permlab_writeGeolocationPermissions">Modify Browser geolocation permissions</string>
     <!-- Description of an application permission, listed so the user can choose whether
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 7258e11..731d1f3 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1646,7 +1646,8 @@
         IAudioService service = getService();
         try {
             status = service.requestAudioFocus(streamType, durationHint, mICallBack,
-                    mAudioFocusDispatcher, getIdForAudioFocusListener(l));
+                    mAudioFocusDispatcher, getIdForAudioFocusListener(l),
+                    mContext.getPackageName() /* package name */);
         } catch (RemoteException e) {
             Log.e(TAG, "Can't call requestAudioFocus() from AudioService due to "+e);
         }
@@ -1682,7 +1683,9 @@
      *      in the application manifest.
      */
     public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
-        //TODO enforce the rule about the receiver being declared in the manifest
+        if (eventReceiver == null) {
+            return;
+        }
         IAudioService service = getService();
         try {
             service.registerMediaButtonEventReceiver(eventReceiver);
@@ -1697,6 +1700,9 @@
      *      that was registered with {@link #registerMediaButtonEventReceiver(ComponentName)}.
      */
     public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
+        if (eventReceiver == null) {
+            return;
+        }
         IAudioService service = getService();
         try {
             service.unregisterMediaButtonEventReceiver(eventReceiver);
@@ -1706,6 +1712,126 @@
     }
 
     /**
+     * @hide
+     * Registers the remote control client for providing information to display on the remotes.
+     * @param eventReceiver identifier of a {@link android.content.BroadcastReceiver}
+     *      that will receive the media button intent, and associated with the remote control
+     *      client. This method has no effect if
+     *      {@link #registerMediaButtonEventReceiver(ComponentName)} hasn't been called
+     *      with the same eventReceiver, or if
+     *      {@link #unregisterMediaButtonEventReceiver(ComponentName)} has been called.
+     * @param rcClient the client associated with the event receiver, responsible for providing
+     *      the information to display on the remote control.
+     */
+    public void registerRemoteControlClient(ComponentName eventReceiver,
+            IRemoteControlClient rcClient) {
+        if (eventReceiver == null) {
+            return;
+        }
+        IAudioService service = getService();
+        try {
+            service.registerRemoteControlClient(eventReceiver, rcClient,
+                    // used to match media button event receiver and audio focus
+                    mContext.getPackageName());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in registerRemoteControlClient"+e);
+        }
+    }
+
+    /**
+     * @hide
+     * @param eventReceiver
+     */
+    public void unregisterRemoteControlClient(ComponentName eventReceiver) {
+        if (eventReceiver == null) {
+            return;
+        }
+        IAudioService service = getService();
+        try {
+            // unregistering a IRemoteControlClient is equivalent to setting it to null
+            service.registerRemoteControlClient(eventReceiver, null, mContext.getPackageName());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in unregisterRemoteControlClient"+e);
+        }
+    }
+
+    /**
+     * @hide
+     * Definitions of constants to be used in {@link android.media.IRemoteControlClient}.
+     */
+    public final class RemoteControlParameters {
+        public final static int PLAYSTATE_STOPPED            = 1;
+        public final static int PLAYSTATE_PAUSED             = 2;
+        public final static int PLAYSTATE_PLAYING            = 3;
+        public final static int PLAYSTATE_FAST_FORWARDING    = 4;
+        public final static int PLAYSTATE_REWINDING          = 5;
+        public final static int PLAYSTATE_SKIPPING_FORWARDS  = 6;
+        public final static int PLAYSTATE_SKIPPING_BACKWARDS = 7;
+        public final static int PLAYSTATE_BUFFERING          = 8;
+
+        public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
+        public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
+        public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
+        public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
+        public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
+        public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
+        public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
+        public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
+    }
+
+    /**
+     * @hide
+     * Broadcast intent action indicating that the displays on the remote controls
+     * should be updated because a new remote control client is now active. If there is no
+     * {@link #EXTRA_REMOTE_CONTROL_CLIENT}, the remote control display should be cleared
+     * because there is no valid client to supply it with information.
+     *
+     * @see #EXTRA_REMOTE_CONTROL_CLIENT
+     */
+    public static final String REMOTE_CONTROL_CLIENT_CHANGED =
+            "android.media.REMOTE_CONTROL_CLIENT_CHANGED";
+
+    /**
+     * @hide
+     * The IRemoteControlClient monotonically increasing generation counter.
+     *
+     * @see #REMOTE_CONTROL_CLIENT_CHANGED_ACTION
+     */
+    public static final String EXTRA_REMOTE_CONTROL_CLIENT =
+            "android.media.EXTRA_REMOTE_CONTROL_CLIENT";
+
+    /**
+     * @hide
+     * FIXME to be changed to address Neel's comments
+     * Force a refresh of the remote control client associated with the event receiver.
+     * @param eventReceiver
+     */
+    public void refreshRemoteControlDisplay(ComponentName eventReceiver) {
+        IAudioService service = getService();
+        try {
+            service.refreshRemoteControlDisplay(eventReceiver);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in refreshRemoteControlDisplay"+e);
+        }
+    }
+
+    /**
+     * @hide
+     * FIXME API to be used by implementors of remote controls, not a candidate for SDK
+     */
+    public void registerRemoteControlObserver() {
+
+    }
+
+    /**
+     * @hide
+     * FIXME API to be used by implementors of remote controls, not a candidate for SDK
+     */
+    public void unregisterRemoteControlObserver() {
+
+    }
+
+    /**
      *  @hide
      *  Reload audio settings. This method is called by Settings backup
      *  agent when audio settings are restored and causes the AudioService
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 682560a..cd55b0e 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -55,6 +55,7 @@
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.lang.ref.SoftReference;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -113,6 +114,8 @@
     private static final int MSG_SET_FORCE_USE = 10;
     private static final int MSG_PERSIST_MEDIABUTTONRECEIVER = 11;
     private static final int MSG_BT_HEADSET_CNCT_FAILED = 12;
+    private static final int MSG_RCDISPLAY_CLEAR = 13;
+    private static final int MSG_RCDISPLAY_UPDATE = 14;
 
     private static final int BTA2DP_DOCK_TIMEOUT_MILLIS = 8000;
     // Timeout for connection to bluetooth headset service
@@ -184,7 +187,7 @@
         AudioSystem.STREAM_RING,  // STREAM_RING
         AudioSystem.STREAM_MUSIC, // STREAM_MUSIC
         AudioSystem.STREAM_ALARM,  // STREAM_ALARM
-        AudioSystem.STREAM_NOTIFICATION,  // STREAM_NOTIFICATION
+        AudioSystem.STREAM_RING,   // STREAM_NOTIFICATION
         AudioSystem.STREAM_BLUETOOTH_SCO, // STREAM_BLUETOOTH_SCO
         AudioSystem.STREAM_SYSTEM,  // STREAM_SYSTEM_ENFORCED
         AudioSystem.STREAM_VOICE_CALL, // STREAM_DTMF
@@ -239,9 +242,6 @@
      */
     private int mVibrateSetting;
 
-    /** @see System#NOTIFICATIONS_USE_RING_VOLUME */
-    private int mNotificationsUseRingVolume;
-
     // Broadcast receiver for device connections intent broadcasts
     private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
 
@@ -371,7 +371,9 @@
 
         // Register for media button intent broadcasts.
         intentFilter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
-        intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        // Workaround for bug on priority setting
+        //intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        intentFilter.setPriority(Integer.MAX_VALUE);
         context.registerReceiver(mMediaButtonReceiver, intentFilter);
 
         // Register for phone state monitoring
@@ -451,16 +453,6 @@
                 System.MUTE_STREAMS_AFFECTED,
                 ((1 << AudioSystem.STREAM_MUSIC)|(1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_SYSTEM)));
 
-        if (mVoiceCapable) {
-            mNotificationsUseRingVolume = System.getInt(cr,
-                    Settings.System.NOTIFICATIONS_USE_RING_VOLUME, 1);
-        } else {
-            mNotificationsUseRingVolume = 1;
-        }
-
-        if (mNotificationsUseRingVolume == 1) {
-            STREAM_VOLUME_ALIAS[AudioSystem.STREAM_NOTIFICATION] = AudioSystem.STREAM_RING;
-        }
         // Each stream will read its own persisted settings
 
         // Broadcast the sticky intent
@@ -885,7 +877,8 @@
                 requestAudioFocus(AudioManager.STREAM_RING,
                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, cb,
                         null /* IAudioFocusDispatcher allowed to be null only for this clientId */,
-                        IN_VOICE_COMM_FOCUS_ID /*clientId*/);
+                        IN_VOICE_COMM_FOCUS_ID /*clientId*/,
+                        "system");
 
             }
         }
@@ -897,7 +890,8 @@
             requestAudioFocus(AudioManager.STREAM_RING,
                     AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, cb,
                     null /* IAudioFocusDispatcher allowed to be null only for this clientId */,
-                    IN_VOICE_COMM_FOCUS_ID /*clientId*/);
+                    IN_VOICE_COMM_FOCUS_ID /*clientId*/,
+                    "system");
         }
         // if exiting call
         else if (newMode == AudioSystem.MODE_NORMAL) {
@@ -2155,6 +2149,33 @@
                     persistMediaButtonReceiver( (ComponentName) msg.obj );
                     break;
 
+                case MSG_RCDISPLAY_CLEAR:
+                    Log.i(TAG, "Clear remote control display");
+                    Intent clearIntent = new Intent(AudioManager.REMOTE_CONTROL_CLIENT_CHANGED);
+                    // no extra means no IRemoteControlClient, which is a request to clear
+                    clearIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+                    mContext.sendBroadcast(clearIntent);
+                    break;
+
+                case MSG_RCDISPLAY_UPDATE:
+                    synchronized(mCurrentRcLock) {
+                        if (mCurrentRcClientRef.get() == null) {
+                            // the remote control display owner has changed between the
+                            // the message to update the display was sent, and the time it
+                            // gets to be processed (now)
+                        } else {
+                            mCurrentRcClientGen++;
+                            Log.i(TAG, "Display/update remote control ");
+                            Intent rcClientIntent = new Intent(
+                                    AudioManager.REMOTE_CONTROL_CLIENT_CHANGED);
+                            rcClientIntent.putExtra(AudioManager.EXTRA_REMOTE_CONTROL_CLIENT,
+                                    mCurrentRcClientGen);
+                            rcClientIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+                            mContext.sendBroadcast(rcClientIntent);
+                        }
+                    }
+                    break;
+
                 case MSG_BT_HEADSET_CNCT_FAILED:
                     resetBluetoothSco();
                     break;
@@ -2168,8 +2189,6 @@
             super(new Handler());
             mContentResolver.registerContentObserver(Settings.System.getUriFor(
                 Settings.System.MODE_RINGER_STREAMS_AFFECTED), false, this);
-            mContentResolver.registerContentObserver(Settings.System.getUriFor(
-                    Settings.System.NOTIFICATIONS_USE_RING_VOLUME), false, this);
         }
 
         @Override
@@ -2193,29 +2212,6 @@
                     mRingerModeAffectedStreams = ringerModeAffectedStreams;
                     setRingerModeInt(getRingerMode(), false);
                 }
-
-                int notificationsUseRingVolume = Settings.System.getInt(mContentResolver,
-                        Settings.System.NOTIFICATIONS_USE_RING_VOLUME,
-                        1);
-                if (mVoiceCapable) {
-                    if (notificationsUseRingVolume != mNotificationsUseRingVolume) {
-                        mNotificationsUseRingVolume = notificationsUseRingVolume;
-                        if (mNotificationsUseRingVolume == 1) {
-                            STREAM_VOLUME_ALIAS[AudioSystem.STREAM_NOTIFICATION] = AudioSystem.STREAM_RING;
-                            mStreamStates[AudioSystem.STREAM_NOTIFICATION].setVolumeIndexSettingName(
-                                    System.VOLUME_SETTINGS[AudioSystem.STREAM_RING]);
-                        } else {
-                            STREAM_VOLUME_ALIAS[AudioSystem.STREAM_NOTIFICATION] = AudioSystem.STREAM_NOTIFICATION;
-                            mStreamStates[AudioSystem.STREAM_NOTIFICATION].setVolumeIndexSettingName(
-                                    System.VOLUME_SETTINGS[AudioSystem.STREAM_NOTIFICATION]);
-                            // Persist notification volume volume as it was not persisted while aliased to ring volume
-                            //  and persist with no delay as there might be registered observers of the persisted
-                            //  notification volume.
-                            sendMsg(mAudioHandler, MSG_PERSIST_VOLUME, AudioSystem.STREAM_NOTIFICATION,
-                                    SENDMSG_REPLACE, 1, 1, mStreamStates[AudioSystem.STREAM_NOTIFICATION], 0);
-                        }
-                    }
-                }
             }
         }
     }
@@ -2567,23 +2563,25 @@
 
     private static class FocusStackEntry {
         public int mStreamType = -1;// no stream type
-        public boolean mIsTransportControlReceiver = false;
         public IAudioFocusDispatcher mFocusDispatcher = null;
         public IBinder mSourceRef = null;
         public String mClientId;
         public int mFocusChangeType;
+        public String mPackageName;
+        public int mCallingUid;
 
         public FocusStackEntry() {
         }
 
-        public FocusStackEntry(int streamType, int duration, boolean isTransportControlReceiver,
-                IAudioFocusDispatcher afl, IBinder source, String id) {
+        public FocusStackEntry(int streamType, int duration,
+                IAudioFocusDispatcher afl, IBinder source, String id, String pn, int uid) {
             mStreamType = streamType;
-            mIsTransportControlReceiver = isTransportControlReceiver;
             mFocusDispatcher = afl;
             mSourceRef = source;
             mClientId = id;
             mFocusChangeType = duration;
+            mPackageName = pn;
+            mCallingUid = uid;
         }
     }
 
@@ -2600,13 +2598,15 @@
             while(stackIterator.hasNext()) {
                 FocusStackEntry fse = stackIterator.next();
                 pw.println("     source:" + fse.mSourceRef + " -- client: " + fse.mClientId
-                        + " -- duration: " +fse.mFocusChangeType);
+                        + " -- duration: " + fse.mFocusChangeType
+                        + " -- uid: " + fse.mCallingUid);
             }
         }
     }
 
     /**
      * Helper function:
+     * Called synchronized on mAudioFocusLock
      * Remove a focus listener from the focus stack.
      * @param focusListenerToRemove the focus listener
      * @param signal if true and the listener was at the top of the focus stack, i.e. it was holding
@@ -2621,6 +2621,10 @@
             if (signal) {
                 // notify the new top of the stack it gained focus
                 notifyTopOfAudioFocusStack();
+                // there's a new top of the stack, let the remote control know
+                synchronized(mRCStack) {
+                    checkUpdateRemoteControlDisplay();
+                }
             }
         } else {
             // focus is abandoned by a client that's not at the top of the stack,
@@ -2639,6 +2643,7 @@
 
     /**
      * Helper function:
+     * Called synchronized on mAudioFocusLock
      * Remove focus listeners from the focus stack for a particular client.
      */
     private void removeFocusStackEntryForClient(IBinder cb) {
@@ -2658,6 +2663,10 @@
             // we removed an entry at the top of the stack:
             //  notify the new top of the stack it gained focus.
             notifyTopOfAudioFocusStack();
+            // there's a new top of the stack, let the remote control know
+            synchronized(mRCStack) {
+                checkUpdateRemoteControlDisplay();
+            }
         }
     }
 
@@ -2700,7 +2709,7 @@
 
     /** @see AudioManager#requestAudioFocus(IAudioFocusDispatcher, int, int) */
     public int requestAudioFocus(int mainStreamType, int focusChangeHint, IBinder cb,
-            IAudioFocusDispatcher fd, String clientId) {
+            IAudioFocusDispatcher fd, String clientId, String callingPackageName) {
         Log.i(TAG, " AudioFocus  requestAudioFocus() from " + clientId);
         // the main stream type for the audio focus request is currently not used. It may
         // potentially be used to handle multiple stream type-dependent audio focuses.
@@ -2743,8 +2752,13 @@
             removeFocusStackEntry(clientId, false);
 
             // push focus requester at the top of the audio focus stack
-            mFocusStack.push(new FocusStackEntry(mainStreamType, focusChangeHint, false, fd, cb,
-                    clientId));
+            mFocusStack.push(new FocusStackEntry(mainStreamType, focusChangeHint, fd, cb,
+                    clientId, callingPackageName, Binder.getCallingUid()));
+
+            // there's a new top of the stack, let the remote control know
+            synchronized(mRCStack) {
+                checkUpdateRemoteControlDisplay();
+            }
         }//synchronized(mAudioFocusLock)
 
         // handle the potential premature death of the new holder of the focus
@@ -2831,19 +2845,100 @@
         }
     }
 
-    private static class RemoteControlStackEntry {
-        public ComponentName mReceiverComponent;// always non null
-        // TODO implement registration expiration?
-        //public int mRegistrationTime;
+    private final static Object mCurrentRcLock = new Object();
+    /**
+     * The one remote control client to be polled for display information.
+     * This object is never null, but its reference might.
+     * Access protected by mCurrentRcLock.
+     */
+    private static SoftReference<IRemoteControlClient> mCurrentRcClientRef =
+            new SoftReference<IRemoteControlClient>(null);
 
-        public RemoteControlStackEntry() {
-        }
+    /**
+     * A monotonically increasing generation counter for mCurrentRcClientRef.
+     * Only accessed with a lock on mCurrentRcLock.
+     */
+    private static int mCurrentRcClientGen = 0;
 
-        public RemoteControlStackEntry(ComponentName r) {
-            mReceiverComponent = r;
+    /**
+     * Returns the current remote control client.
+     * @param rcClientId the counter value that matches the extra
+     *     {@link AudioManager#EXTRA_REMOTE_CONTROL_CLIENT} in the
+     *     {@link AudioManager#REMOTE_CONTROL_CLIENT_CHANGED} event
+     * @return the current IRemoteControlClient from which information to display on the remote
+     *     control can be retrieved, or null if rcClientId doesn't match the current generation
+     *     counter.
+     */
+    public static IRemoteControlClient getRemoteControlClient(int rcClientId) {
+        synchronized(mCurrentRcLock) {
+            if (rcClientId == mCurrentRcClientGen) {
+                return mCurrentRcClientRef.get();
+            } else {
+                return null;
+            }
         }
     }
 
+    /**
+     * Inner class to monitor remote control client deaths, and remove the client for the
+     * remote control stack if necessary.
+     */
+    private class RcClientDeathHandler implements IBinder.DeathRecipient {
+        private IBinder mCb; // To be notified of client's death
+        private ComponentName mRcEventReceiver;
+
+        RcClientDeathHandler(IBinder cb, ComponentName eventReceiver) {
+            mCb = cb;
+            mRcEventReceiver = eventReceiver;
+        }
+
+        public void binderDied() {
+            Log.w(TAG, "  RemoteControlClient died");
+            // remote control client died, make sure the displays don't use it anymore
+            //  by setting its remote control client to null
+            registerRemoteControlClient(mRcEventReceiver, null, null/*ignored*/);
+        }
+
+        public IBinder getBinder() {
+            return mCb;
+        }
+    }
+
+    private static class RemoteControlStackEntry {
+        /** the target for the ACTION_MEDIA_BUTTON events */
+        public ComponentName mReceiverComponent;// always non null
+        public String mCallingPackageName;
+        public int mCallingUid;
+
+        /** provides access to the information to display on the remote control */
+        public SoftReference<IRemoteControlClient> mRcClientRef;
+        public RcClientDeathHandler mRcClientDeathHandler;
+
+        public RemoteControlStackEntry(ComponentName r) {
+            mReceiverComponent = r;
+            mCallingUid = -1;
+            mRcClientRef = new SoftReference<IRemoteControlClient>(null);
+        }
+
+        public void unlinkToRcClientDeath() {
+            if ((mRcClientDeathHandler != null) && (mRcClientDeathHandler.mCb != null)) {
+                try {
+                    mRcClientDeathHandler.mCb.unlinkToDeath(mRcClientDeathHandler, 0);
+                } catch (java.util.NoSuchElementException e) {
+                    // not much we can do here
+                    Log.e(TAG, "Encountered " + e + " in unlinkToRcClientDeath()");
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     *  The stack of remote control event receivers.
+     *  Code sections and methods that modify the remote control event receiver stack are
+     *  synchronized on mRCStack, but also BEFORE on mFocusLock as any change in either
+     *  stack, audio focus or RC, can lead to a change in the remote control display
+     */
     private Stack<RemoteControlStackEntry> mRCStack = new Stack<RemoteControlStackEntry>();
 
     /**
@@ -2855,8 +2950,10 @@
         synchronized(mRCStack) {
             Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
             while(stackIterator.hasNext()) {
-                RemoteControlStackEntry fse = stackIterator.next();
-                pw.println("     receiver:" + fse.mReceiverComponent);
+                RemoteControlStackEntry rcse = stackIterator.next();
+                pw.println("     receiver: " + rcse.mReceiverComponent +
+                        "  -- client: " + rcse.mRcClientRef.get() +
+                        "  -- uid: " + rcse.mCallingUid);
             }
         }
     }
@@ -2909,6 +3006,7 @@
             ComponentName receiverComponentName = ComponentName.unflattenFromString(receiverName);
             registerMediaButtonEventReceiver(receiverComponentName);
         }
+        // upon restoring (e.g. after boot), do we want to refresh all remotes?
     }
 
     /**
@@ -2921,14 +3019,20 @@
             return;
         }
         Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+        RemoteControlStackEntry rcse = null;
+        boolean wasInsideStack = false;
         while(stackIterator.hasNext()) {
-            RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
+            rcse = (RemoteControlStackEntry)stackIterator.next();
             if(rcse.mReceiverComponent.equals(newReceiver)) {
+                wasInsideStack = true;
                 stackIterator.remove();
                 break;
             }
         }
-        mRCStack.push(new RemoteControlStackEntry(newReceiver));
+        if (!wasInsideStack) {
+            rcse = new RemoteControlStackEntry(newReceiver);
+        }
+        mRCStack.push(rcse);
 
         // post message to persist the default media button receiver
         mAudioHandler.sendMessage( mAudioHandler.obtainMessage(
@@ -2950,13 +3054,88 @@
         }
     }
 
+    /**
+     * Helper function:
+     * Called synchronized on mRCStack
+     */
+    private boolean isCurrentRcController(ComponentName eventReceiver) {
+        if (!mRCStack.empty() && mRCStack.peek().mReceiverComponent.equals(eventReceiver)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Helper function:
+     * Called synchronized on mRCStack
+     */
+    private void clearRemoteControlDisplay() {
+        synchronized(mCurrentRcLock) {
+            mCurrentRcClientRef.clear();
+        }
+        mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_CLEAR) );
+    }
+
+    /**
+     * Helper function:
+     * Called synchronized on mRCStack
+     * mRCStack.empty() is false
+     */
+    private void updateRemoteControlDisplay() {
+        RemoteControlStackEntry rcse = mRCStack.peek();
+        // this is where we enforce opt-in for information display on the remote controls
+        //   with the new AudioManager.registerRemoteControlClient() API
+        if (rcse.mRcClientRef.get() == null) {
+            // FIXME remove log before release: this warning will be displayed for every AF change
+            Log.w(TAG, "Can't update remote control display with null remote control client");
+            clearRemoteControlDisplay();
+            return;
+        }
+        synchronized(mCurrentRcLock) {
+            mCurrentRcClientRef = rcse.mRcClientRef;
+        }
+        mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_UPDATE, 0, 0, rcse) );
+    }
+
+    /**
+     * Helper function:
+     * Called synchronized on mFocusLock, then mRCStack
+     * Check whether the remote control display should be updated, triggers the update if required
+     */
+    private void checkUpdateRemoteControlDisplay() {
+        // determine whether the remote control display should be refreshed
+        // if either stack is empty, there is a mismatch, so clear the RC display
+        if (mRCStack.isEmpty() || mFocusStack.isEmpty()) {
+            clearRemoteControlDisplay();
+            return;
+        }
+        // if the top of the two stacks belong to different packages, there is a mismatch, clear
+        if ((mRCStack.peek().mCallingPackageName != null)
+                && (mFocusStack.peek().mPackageName != null)
+                && !(mRCStack.peek().mCallingPackageName.compareTo(
+                        mFocusStack.peek().mPackageName) == 0)) {
+            clearRemoteControlDisplay();
+            return;
+        }
+        // if the audio focus didn't originate from the same Uid as the one in which the remote
+        //   control information will be retrieved, clear
+        if (mRCStack.peek().mCallingUid != mFocusStack.peek().mCallingUid) {
+            clearRemoteControlDisplay();
+            return;
+        }
+        // refresh conditions were verified: update the remote controls
+        updateRemoteControlDisplay();
+    }
 
     /** see AudioManager.registerMediaButtonEventReceiver(ComponentName eventReceiver) */
     public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
         Log.i(TAG, "  Remote Control   registerMediaButtonEventReceiver() for " + eventReceiver);
 
-        synchronized(mRCStack) {
-            pushMediaButtonReceiver(eventReceiver);
+        synchronized(mAudioFocusLock) {
+            synchronized(mRCStack) {
+                pushMediaButtonReceiver(eventReceiver);
+                checkUpdateRemoteControlDisplay();
+            }
         }
     }
 
@@ -2964,11 +3143,74 @@
     public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
         Log.i(TAG, "  Remote Control   unregisterMediaButtonEventReceiver() for " + eventReceiver);
 
-        synchronized(mRCStack) {
-            removeMediaButtonReceiver(eventReceiver);
+        synchronized(mAudioFocusLock) {
+            synchronized(mRCStack) {
+                boolean topOfStackWillChange = isCurrentRcController(eventReceiver);
+                removeMediaButtonReceiver(eventReceiver);
+                if (topOfStackWillChange) {
+                    checkUpdateRemoteControlDisplay();
+                }
+            }
         }
     }
 
+    /** see AudioManager.registerRemoteControlClient(ComponentName eventReceiver, ...) */
+    public void registerRemoteControlClient(ComponentName eventReceiver,
+            IRemoteControlClient rcClient, String callingPackageName) {
+        synchronized(mAudioFocusLock) {
+            synchronized(mRCStack) {
+                // store the new display information
+                Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+                while(stackIterator.hasNext()) {
+                    RemoteControlStackEntry rcse = stackIterator.next();
+                    if(rcse.mReceiverComponent.equals(eventReceiver)) {
+                        // already had a remote control client?
+                        if (rcse.mRcClientDeathHandler != null) {
+                            // stop monitoring the old client's death
+                            rcse.unlinkToRcClientDeath();
+                        }
+                        // save the new remote control client
+                        rcse.mRcClientRef = new SoftReference<IRemoteControlClient>(rcClient);
+                        rcse.mCallingPackageName = callingPackageName;
+                        rcse.mCallingUid = Binder.getCallingUid();
+                        if (rcClient == null) {
+                            break;
+                        }
+                        // monitor the new client's death
+                        IBinder b = rcClient.asBinder();
+                        RcClientDeathHandler rcdh =
+                                new RcClientDeathHandler(b, rcse.mReceiverComponent);
+                        try {
+                            b.linkToDeath(rcdh, 0);
+                        } catch (RemoteException e) {
+                            // remote control client is DOA, disqualify it
+                            Log.w(TAG, "registerRemoteControlClient() has a dead client " + b);
+                            rcse.mRcClientRef.clear();
+                        }
+                        rcse.mRcClientDeathHandler = rcdh;
+                        break;
+                    }
+                }
+                // if the eventReceiver is at the top of the stack
+                // then check for potential refresh of the remote controls
+                if (isCurrentRcController(eventReceiver)) {
+                    checkUpdateRemoteControlDisplay();
+                }
+            }
+        }
+    }
+
+    /** see AudioManager.refreshRemoteControlDisplay(ComponentName er) */
+    public void refreshRemoteControlDisplay(ComponentName eventReceiver) {
+        synchronized(mAudioFocusLock) {
+            synchronized(mRCStack) {
+                // only refresh if the eventReceiver is at the top of the stack
+                if (isCurrentRcController(eventReceiver)) {
+                    checkUpdateRemoteControlDisplay();
+                }
+            }
+        }
+    }
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index e3bd7b4..1a05f152 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -18,6 +18,9 @@
 
 import android.content.ComponentName;
 import android.media.IAudioFocusDispatcher;
+import android.media.IRemoteControlClient;
+import android.net.Uri;
+import android.os.Bundle;
 
 /**
  * {@hide}
@@ -77,7 +80,7 @@
     boolean isBluetoothScoOn();
 
     int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb, IAudioFocusDispatcher l,
-            String clientId);
+            String clientId, String callingPackageName);
 
     int abandonAudioFocus(IAudioFocusDispatcher l, String clientId);
     
@@ -87,6 +90,11 @@
 
     void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver);
 
+    void registerRemoteControlClient(in ComponentName eventReceiver,
+           in IRemoteControlClient rcClient, in String callingPackageName);
+
+    void refreshRemoteControlDisplay(in ComponentName eventReceiver);
+
     void startBluetoothSco(IBinder cb);
 
     void stopBluetoothSco(IBinder cb);
diff --git a/media/java/android/media/IRemoteControlClient.aidl b/media/java/android/media/IRemoteControlClient.aidl
new file mode 100644
index 0000000..a49371c
--- /dev/null
+++ b/media/java/android/media/IRemoteControlClient.aidl
@@ -0,0 +1,76 @@
+/*
+ * 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.media;
+
+import android.graphics.Bitmap;
+
+/**
+ * {@hide}
+ */
+interface IRemoteControlClient
+{
+    /**
+     * Called by a remote control to retrieve a String of information to display.
+     * @param field the identifier for a metadata field to retrieve. Valid values are
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUM},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_ARTIST},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_AUTHOR},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPILATION},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPOSER},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_DATE},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_DURATION},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_GENRE},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_WRITER},
+     *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_YEAR}.
+     * @return null if the given field is not supported, or the String matching the metadata field.
+     */
+    String getMetadataString(int field);
+
+    /**
+     * Returns the current playback state.
+     * @return one of the following values:
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_STOPPED},
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_PAUSED},
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_PLAYING},
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_FAST_FORWARDING},
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_REWINDING},
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_SKIPPING_FORWARDS},
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_SKIPPING_BACKWARDS},
+     *       {@link android.media.AudioManager.RemoteControl#PLAYSTATE_BUFFERING}.
+     */
+    int getPlaybackState();
+
+    /**
+     * Returns the flags for the media transport control buttons this client supports.
+     * @see {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PREVIOUS},
+     *      {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_REWIND},
+     *      {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PLAY},
+     *      {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PLAY_PAUSE},
+     *      {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PAUSE},
+     *      {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_STOP},
+     *      {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_FAST_FORWARD},
+     *      {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_NEXT}
+     */
+    int getTransportControlFlags();
+
+    Bitmap getAlbumArt(int width, int height);
+}
diff --git a/services/audioflinger/AudioFlinger.cpp b/services/audioflinger/AudioFlinger.cpp
index ec45530..2355d5c 100644
--- a/services/audioflinger/AudioFlinger.cpp
+++ b/services/audioflinger/AudioFlinger.cpp
@@ -51,6 +51,8 @@
 
 #include <media/EffectsFactoryApi.h>
 #include <audio_effects/effect_visualizer.h>
+#include <audio_effects/effect_ns.h>
+#include <audio_effects/effect_aec.h>
 
 #include <cpustats/ThreadCpuUsage.h>
 #include <powermanager/PowerManager.h>
@@ -148,7 +150,8 @@
 
 AudioFlinger::AudioFlinger()
     : BnAudioFlinger(),
-        mPrimaryHardwareDev(0), mMasterVolume(1.0f), mMasterMute(false), mNextUniqueId(1)
+        mPrimaryHardwareDev(0), mMasterVolume(1.0f), mMasterMute(false), mNextUniqueId(1),
+        mBtNrec(false)
 {
 }
 
@@ -717,6 +720,31 @@
             final_result = result ?: final_result;
         }
         mHardwareStatus = AUDIO_HW_IDLE;
+        // disable AEC and NS if the device is a BT SCO headset supporting those pre processings
+        AudioParameter param = AudioParameter(keyValuePairs);
+        String8 value;
+        if (param.get(String8(AUDIO_PARAMETER_KEY_BT_NREC), value) == NO_ERROR) {
+            Mutex::Autolock _l(mLock);
+            bool btNrec = (value == AUDIO_PARAMETER_VALUE_ON);
+            if (mBtNrec != btNrec) {
+                for (size_t i = 0; i < mRecordThreads.size(); i++) {
+                    sp<RecordThread> thread = mRecordThreads.valueAt(i);
+                    RecordThread::RecordTrack *track = thread->track();
+                    if (track != NULL) {
+                        audio_devices_t device = (audio_devices_t)(
+                                thread->device() & AUDIO_DEVICE_IN_ALL);
+                        bool suspend = audio_is_bluetooth_sco_device(device) && btNrec;
+                        thread->setEffectSuspended(FX_IID_AEC,
+                                                   suspend,
+                                                   track->sessionId());
+                        thread->setEffectSuspended(FX_IID_NS,
+                                                   suspend,
+                                                   track->sessionId());
+                    }
+                }
+                mBtNrec = btNrec;
+            }
+        }
         return final_result;
     }
 
@@ -1130,6 +1158,140 @@
     LOGW("power manager service died !!!");
 }
 
+void AudioFlinger::ThreadBase::setEffectSuspended(
+        const effect_uuid_t *type, bool suspend, int sessionId)
+{
+    Mutex::Autolock _l(mLock);
+    setEffectSuspended_l(type, suspend, sessionId);
+}
+
+void AudioFlinger::ThreadBase::setEffectSuspended_l(
+        const effect_uuid_t *type, bool suspend, int sessionId)
+{
+    sp<EffectChain> chain;
+    chain = getEffectChain_l(sessionId);
+    if (chain != 0) {
+        if (type != NULL) {
+            chain->setEffectSuspended_l(type, suspend);
+        } else {
+            chain->setEffectSuspendedAll_l(suspend);
+        }
+    }
+
+    updateSuspendedSessions_l(type, suspend, sessionId);
+}
+
+void AudioFlinger::ThreadBase::checkSuspendOnAddEffectChain_l(const sp<EffectChain>& chain)
+{
+    int index = mSuspendedSessions.indexOfKey(chain->sessionId());
+    if (index < 0) {
+        return;
+    }
+
+    KeyedVector <int, sp<SuspendedSessionDesc> > sessionEffects =
+            mSuspendedSessions.editValueAt(index);
+
+    for (size_t i = 0; i < sessionEffects.size(); i++) {
+        sp <SuspendedSessionDesc> desc = sessionEffects.valueAt(i);
+        for (int j = 0; j < desc->mRefCount; j++) {
+            if (sessionEffects.keyAt(i) == EffectChain::kKeyForSuspendAll) {
+                chain->setEffectSuspendedAll_l(true);
+            } else {
+                LOGV("checkSuspendOnAddEffectChain_l() suspending effects %08x",
+                     desc->mType.timeLow);
+                chain->setEffectSuspended_l(&desc->mType, true);
+            }
+        }
+    }
+}
+
+void AudioFlinger::ThreadBase::updateSuspendedSessionsOnRemoveEffectChain_l(
+        const sp<EffectChain>& chain)
+{
+    int index = mSuspendedSessions.indexOfKey(chain->sessionId());
+    if (index < 0) {
+        return;
+    }
+    LOGV("updateSuspendedSessionsOnRemoveEffectChain_l() removed suspended session %d",
+         chain->sessionId());
+    mSuspendedSessions.removeItemsAt(index);
+}
+
+void AudioFlinger::ThreadBase::updateSuspendedSessions_l(const effect_uuid_t *type,
+                                                         bool suspend,
+                                                         int sessionId)
+{
+    int index = mSuspendedSessions.indexOfKey(sessionId);
+
+    KeyedVector <int, sp<SuspendedSessionDesc> > sessionEffects;
+
+    if (suspend) {
+        if (index >= 0) {
+            sessionEffects = mSuspendedSessions.editValueAt(index);
+        } else {
+            mSuspendedSessions.add(sessionId, sessionEffects);
+        }
+    } else {
+        if (index < 0) {
+            return;
+        }
+        sessionEffects = mSuspendedSessions.editValueAt(index);
+    }
+
+
+    int key = EffectChain::kKeyForSuspendAll;
+    if (type != NULL) {
+        key = type->timeLow;
+    }
+    index = sessionEffects.indexOfKey(key);
+
+    sp <SuspendedSessionDesc> desc;
+    if (suspend) {
+        if (index >= 0) {
+            desc = sessionEffects.valueAt(index);
+        } else {
+            desc = new SuspendedSessionDesc();
+            if (type != NULL) {
+                memcpy(&desc->mType, type, sizeof(effect_uuid_t));
+            }
+            sessionEffects.add(key, desc);
+            LOGV("updateSuspendedSessions_l() suspend adding effect %08x", key);
+        }
+        desc->mRefCount++;
+    } else {
+        if (index < 0) {
+            return;
+        }
+        desc = sessionEffects.valueAt(index);
+        if (--desc->mRefCount == 0) {
+            LOGV("updateSuspendedSessions_l() restore removing effect %08x", key);
+            sessionEffects.removeItemsAt(index);
+            if (sessionEffects.isEmpty()) {
+                LOGV("updateSuspendedSessions_l() restore removing session %d",
+                                 sessionId);
+                mSuspendedSessions.removeItem(sessionId);
+            }
+        }
+    }
+    if (!sessionEffects.isEmpty()) {
+        mSuspendedSessions.replaceValueFor(sessionId, sessionEffects);
+    }
+}
+
+void AudioFlinger::ThreadBase::checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+                                                            bool enabled,
+                                                            int sessionId)
+{
+    Mutex::Autolock _l(mLock);
+
+    // TODO: implement PlaybackThread or RecordThread specific behavior here
+
+    sp<EffectChain> chain = getEffectChain_l(sessionId);
+    if (chain != 0) {
+        chain->checkSuspendOnEffectEnabled(effect, enabled);
+    }
+}
+
 // ----------------------------------------------------------------------------
 
 AudioFlinger::PlaybackThread::PlaybackThread(const sp<AudioFlinger>& audioFlinger,
@@ -4143,7 +4305,11 @@
         }
 
         mTrack = track.get();
-
+        // disable AEC and NS if the device is a BT SCO headset supporting those pre processings
+        bool suspend = audio_is_bluetooth_sco_device(
+                (audio_devices_t)(mDevice & AUDIO_DEVICE_IN_ALL)) && mAudioFlinger->btNrec();
+        setEffectSuspended_l(FX_IID_AEC, suspend, sessionId);
+        setEffectSuspended_l(FX_IID_NS, suspend, sessionId);
     }
     lStatus = NO_ERROR;
 
@@ -4363,6 +4529,13 @@
                 status = BAD_VALUE;
             } else {
                 mDevice &= (uint32_t)~(value & AUDIO_DEVICE_IN_ALL);
+                // disable AEC and NS if the device is a BT SCO headset supporting those pre processings
+                if (mTrack != NULL) {
+                    bool suspend = audio_is_bluetooth_sco_device(
+                            (audio_devices_t)value) && mAudioFlinger->btNrec();
+                    setEffectSuspended_l(FX_IID_AEC, suspend, mTrack->sessionId());
+                    setEffectSuspended_l(FX_IID_NS, suspend, mTrack->sessionId());
+                }
             }
             mDevice |= (uint32_t)value;
         }
@@ -4490,6 +4663,12 @@
     return result;
 }
 
+AudioFlinger::RecordThread::RecordTrack* AudioFlinger::RecordThread::track()
+{
+    Mutex::Autolock _l(mLock);
+    return mTrack;
+}
+
 // ----------------------------------------------------------------------------
 
 int AudioFlinger::openOutput(uint32_t *pDevices,
@@ -4874,10 +5053,6 @@
 }
 
 
-// this UUID must match the one defined in media/libeffects/EffectVisualizer.cpp
-static const effect_uuid_t VISUALIZATION_UUID_ =
-    {0xd069d9e0, 0x8329, 0x11df, 0x9168, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b}};
-
 sp<IEffect> AudioFlinger::createEffect(pid_t pid,
         effect_descriptor_t *pDesc,
         const sp<IEffectClient>& effectClient,
@@ -4915,14 +5090,6 @@
         goto Exit;
     }
 
-    // check recording permission for visualizer
-    if ((memcmp(&pDesc->type, SL_IID_VISUALIZATION, sizeof(effect_uuid_t)) == 0 ||
-         memcmp(&pDesc->uuid, &VISUALIZATION_UUID_, sizeof(effect_uuid_t)) == 0) &&
-        !recordingAllowed()) {
-        lStatus = PERMISSION_DENIED;
-        goto Exit;
-    }
-
     if (io == 0) {
         if (sessionId == AUDIO_SESSION_OUTPUT_STAGE) {
             // output must be specified by AudioPolicyManager when using session
@@ -5003,6 +5170,13 @@
             goto Exit;
         }
 
+        // check recording permission for visualizer
+        if ((memcmp(&desc.type, SL_IID_VISUALIZATION, sizeof(effect_uuid_t)) == 0) &&
+            !recordingAllowed()) {
+            lStatus = PERMISSION_DENIED;
+            goto Exit;
+        }
+
         // return effect descriptor
         memcpy(pDesc, &desc, sizeof(effect_descriptor_t));
 
@@ -5069,10 +5243,10 @@
     return handle;
 }
 
-status_t AudioFlinger::moveEffects(int session, int srcOutput, int dstOutput)
+status_t AudioFlinger::moveEffects(int sessionId, int srcOutput, int dstOutput)
 {
     LOGV("moveEffects() session %d, srcOutput %d, dstOutput %d",
-            session, srcOutput, dstOutput);
+            sessionId, srcOutput, dstOutput);
     Mutex::Autolock _l(mLock);
     if (srcOutput == dstOutput) {
         LOGW("moveEffects() same dst and src outputs %d", dstOutput);
@@ -5091,24 +5265,24 @@
 
     Mutex::Autolock _dl(dstThread->mLock);
     Mutex::Autolock _sl(srcThread->mLock);
-    moveEffectChain_l(session, srcThread, dstThread, false);
+    moveEffectChain_l(sessionId, srcThread, dstThread, false);
 
     return NO_ERROR;
 }
 
 // moveEffectChain_l mustbe called with both srcThread and dstThread mLocks held
-status_t AudioFlinger::moveEffectChain_l(int session,
+status_t AudioFlinger::moveEffectChain_l(int sessionId,
                                    AudioFlinger::PlaybackThread *srcThread,
                                    AudioFlinger::PlaybackThread *dstThread,
                                    bool reRegister)
 {
     LOGV("moveEffectChain_l() session %d from thread %p to thread %p",
-            session, srcThread, dstThread);
+            sessionId, srcThread, dstThread);
 
-    sp<EffectChain> chain = srcThread->getEffectChain_l(session);
+    sp<EffectChain> chain = srcThread->getEffectChain_l(sessionId);
     if (chain == 0) {
         LOGW("moveEffectChain_l() effect chain for session %d not on source thread %p",
-                session, srcThread);
+                sessionId, srcThread);
         return INVALID_OPERATION;
     }
 
@@ -5143,7 +5317,7 @@
             AudioSystem::registerEffect(&effect->desc(),
                                         dstOutput,
                                         strategy,
-                                        session,
+                                        sessionId,
                                         effect->id());
         }
         effect = chain->getEffectFromId_l(0);
@@ -5385,6 +5559,7 @@
 
 void AudioFlinger::ThreadBase::disconnectEffect(const sp<EffectModule>& effect,
                                                     const wp<EffectHandle>& handle) {
+
     Mutex::Autolock _l(mLock);
     LOGV("disconnectEffect() %p effect %p", this, effect.get());
     // delete the effect module if removing last handle on it
@@ -5451,6 +5626,7 @@
         if (mEffectChains[i]->sessionId() < session) break;
     }
     mEffectChains.insertAt(chain, i);
+    checkSuspendOnAddEffectChain_l(chain);
 
     return NO_ERROR;
 }
@@ -5463,6 +5639,7 @@
 
     for (size_t i = 0; i < mEffectChains.size(); i++) {
         if (chain == mEffectChains[i]) {
+            updateSuspendedSessionsOnRemoveEffectChain_l(chain);
             mEffectChains.removeAt(i);
             // detach all active tracks from the chain
             for (size_t i = 0 ; i < mActiveTracks.size() ; ++i) {
@@ -5540,6 +5717,8 @@
     chain->setInBuffer(NULL);
     chain->setOutBuffer(NULL);
 
+    checkSuspendOnAddEffectChain_l(chain);
+
     mEffectChains.add(chain);
 
     return NO_ERROR;
@@ -5552,6 +5731,7 @@
             "removeEffectChain_l() %p invalid chain size %d on thread %p",
             chain.get(), mEffectChains.size(), this);
     if (mEffectChains.size() == 1) {
+        updateSuspendedSessionsOnRemoveEffectChain_l(chain);
         mEffectChains.removeAt(0);
     }
     return 0;
@@ -5570,7 +5750,7 @@
                                         int id,
                                         int sessionId)
     : mThread(wThread), mChain(chain), mId(id), mSessionId(sessionId), mEffectInterface(NULL),
-      mStatus(NO_INIT), mState(IDLE)
+      mStatus(NO_INIT), mState(IDLE), mSuspended(false)
 {
     LOGV("Constructor %p", this);
     int lStatus;
@@ -5634,14 +5814,17 @@
     }
     // if inserted in first place, move effect control from previous owner to this handle
     if (i == 0) {
+        bool enabled = false;
         if (h != 0) {
-            h->setControl(false, true);
+            enabled = h->enabled();
+            h->setControl(false/*hasControl*/, true /*signal*/, enabled /*enabled*/);
         }
-        handle->setControl(true, false);
+        handle->setControl(true /*hasControl*/, false /*signal*/, enabled /*enabled*/);
         status = NO_ERROR;
     } else {
         status = ALREADY_EXISTS;
     }
+    LOGV("addHandle() %p added handle %p in position %d", this, handle.get(), i);
     mHandles.insertAt(handle, i);
     return status;
 }
@@ -5657,13 +5840,21 @@
     if (i == size) {
         return size;
     }
+    LOGV("removeHandle() %p removed handle %p in position %d", this, handle.unsafe_get(), i);
+
+    bool enabled = false;
+    EffectHandle *hdl = handle.unsafe_get();
+    if (hdl) {
+        LOGV("removeHandle() unsafe_get OK");
+        enabled = hdl->enabled();
+    }
     mHandles.removeAt(i);
     size = mHandles.size();
     // if removed from first place, move effect control from this handle to next in line
     if (i == 0 && size != 0) {
         sp<EffectHandle> h = mHandles[0].promote();
         if (h != 0) {
-            h->setControl(true, true);
+            h->setControl(true /*hasControl*/, true /*signal*/ , enabled /*enabled*/);
         }
     }
 
@@ -5677,8 +5868,21 @@
     return size;
 }
 
+sp<AudioFlinger::EffectHandle> AudioFlinger::EffectModule::controlHandle()
+{
+    Mutex::Autolock _l(mLock);
+    sp<EffectHandle> handle;
+    if (mHandles.size() != 0) {
+        handle = mHandles[0].promote();
+    }
+    return handle;
+}
+
+
+
 void AudioFlinger::EffectModule::disconnect(const wp<EffectHandle>& handle)
 {
+    LOGV("disconnect() %p handle %p ", this, handle.unsafe_get());
     // keep a strong reference on this EffectModule to avoid calling the
     // destructor before we exit
     sp<EffectModule> keep(this);
@@ -6139,6 +6343,17 @@
     return status;
 }
 
+void AudioFlinger::EffectModule::setSuspended(bool suspended)
+{
+    Mutex::Autolock _l(mLock);
+    mSuspended = suspended;
+}
+bool AudioFlinger::EffectModule::suspended()
+{
+    Mutex::Autolock _l(mLock);
+    return mSuspended;
+}
+
 status_t AudioFlinger::EffectModule::dump(int fd, const Vector<String16>& args)
 {
     const size_t SIZE = 256;
@@ -6235,7 +6450,8 @@
                                         const sp<IEffectClient>& effectClient,
                                         int32_t priority)
     : BnEffect(),
-    mEffect(effect), mEffectClient(effectClient), mClient(client), mPriority(priority), mHasControl(false)
+    mEffect(effect), mEffectClient(effectClient), mClient(client),
+    mPriority(priority), mHasControl(false), mEnabled(false)
 {
     LOGV("constructor %p", this);
 
@@ -6258,30 +6474,66 @@
 {
     LOGV("Destructor %p", this);
     disconnect();
+    LOGV("Destructor DONE %p", this);
 }
 
 status_t AudioFlinger::EffectHandle::enable()
 {
+    LOGV("enable %p", this);
     if (!mHasControl) return INVALID_OPERATION;
     if (mEffect == 0) return DEAD_OBJECT;
 
+    mEnabled = true;
+
+    sp<ThreadBase> thread = mEffect->thread().promote();
+    if (thread != 0) {
+        thread->checkSuspendOnEffectEnabled(mEffect, true, mEffect->sessionId());
+    }
+
+    // checkSuspendOnEffectEnabled() can suspend this same effect when enabled
+    if (mEffect->suspended()) {
+        return NO_ERROR;
+    }
+
     return mEffect->setEnabled(true);
 }
 
 status_t AudioFlinger::EffectHandle::disable()
 {
+    LOGV("disable %p", this);
     if (!mHasControl) return INVALID_OPERATION;
-    if (mEffect == NULL) return DEAD_OBJECT;
+    if (mEffect == 0) return DEAD_OBJECT;
 
-    return mEffect->setEnabled(false);
+    mEnabled = false;
+
+    if (mEffect->suspended()) {
+        return NO_ERROR;
+    }
+
+    status_t status = mEffect->setEnabled(false);
+
+    sp<ThreadBase> thread = mEffect->thread().promote();
+    if (thread != 0) {
+        thread->checkSuspendOnEffectEnabled(mEffect, false, mEffect->sessionId());
+    }
+
+    return status;
 }
 
 void AudioFlinger::EffectHandle::disconnect()
 {
+    LOGV("disconnect %p", this);
     if (mEffect == 0) {
         return;
     }
+
     mEffect->disconnect(this);
+
+    sp<ThreadBase> thread = mEffect->thread().promote();
+    if (thread != 0) {
+        thread->checkSuspendOnEffectEnabled(mEffect, false, mEffect->sessionId());
+    }
+
     // release sp on module => module destructor can be called now
     mEffect.clear();
     if (mCblk) {
@@ -6373,11 +6625,13 @@
     return mCblkMemory;
 }
 
-void AudioFlinger::EffectHandle::setControl(bool hasControl, bool signal)
+void AudioFlinger::EffectHandle::setControl(bool hasControl, bool signal, bool enabled)
 {
     LOGV("setControl %p control %d", this, hasControl);
 
     mHasControl = hasControl;
+    mEnabled = enabled;
+
     if (signal && mEffectClient != 0) {
         mEffectClient->controlStatusChanged(hasControl);
     }
@@ -6448,7 +6702,7 @@
 
 }
 
-// getEffectFromDesc_l() must be called with PlaybackThread::mLock held
+// getEffectFromDesc_l() must be called with ThreadBase::mLock held
 sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectFromDesc_l(effect_descriptor_t *descriptor)
 {
     sp<EffectModule> effect;
@@ -6463,7 +6717,7 @@
     return effect;
 }
 
-// getEffectFromId_l() must be called with PlaybackThread::mLock held
+// getEffectFromId_l() must be called with ThreadBase::mLock held
 sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectFromId_l(int id)
 {
     sp<EffectModule> effect;
@@ -6479,6 +6733,22 @@
     return effect;
 }
 
+// getEffectFromType_l() must be called with ThreadBase::mLock held
+sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectFromType_l(
+        const effect_uuid_t *type)
+{
+    sp<EffectModule> effect;
+    size_t size = mEffects.size();
+
+    for (size_t i = 0; i < size; i++) {
+        if (memcmp(&mEffects[i]->desc().type, type, sizeof(effect_uuid_t)) == 0) {
+            effect = mEffects[i];
+            break;
+        }
+    }
+    return effect;
+}
+
 // Must be called with EffectChain::mLock locked
 void AudioFlinger::EffectChain::process_l()
 {
@@ -6773,6 +7043,166 @@
     return NO_ERROR;
 }
 
+// must be called with ThreadBase::mLock held
+void AudioFlinger::EffectChain::setEffectSuspended_l(
+        const effect_uuid_t *type, bool suspend)
+{
+    sp<SuspendedEffectDesc> desc;
+    // use effect type UUID timelow as key as there is no real risk of identical
+    // timeLow fields among effect type UUIDs.
+    int index = mSuspendedEffects.indexOfKey(type->timeLow);
+    if (suspend) {
+        if (index >= 0) {
+            desc = mSuspendedEffects.valueAt(index);
+        } else {
+            desc = new SuspendedEffectDesc();
+            memcpy(&desc->mType, type, sizeof(effect_uuid_t));
+            mSuspendedEffects.add(type->timeLow, desc);
+            LOGV("setEffectSuspended_l() add entry for %08x", type->timeLow);
+        }
+        if (desc->mRefCount++ == 0) {
+            sp<EffectModule> effect = getEffectIfEnabled(type);
+            if (effect != 0) {
+                desc->mEffect = effect;
+                effect->setSuspended(true);
+                effect->setEnabled(false);
+            }
+        }
+    } else {
+        if (index < 0) {
+            return;
+        }
+        desc = mSuspendedEffects.valueAt(index);
+        if (desc->mRefCount <= 0) {
+            LOGW("setEffectSuspended_l() restore refcount should not be 0 %d", desc->mRefCount);
+            desc->mRefCount = 1;
+        }
+        if (--desc->mRefCount == 0) {
+            LOGV("setEffectSuspended_l() remove entry for %08x", mSuspendedEffects.keyAt(index));
+            if (desc->mEffect != 0) {
+                sp<EffectModule> effect = desc->mEffect.promote();
+                if (effect != 0) {
+                    effect->setSuspended(false);
+                    sp<EffectHandle> handle = effect->controlHandle();
+                    if (handle != 0) {
+                        effect->setEnabled(handle->enabled());
+                    }
+                }
+                desc->mEffect.clear();
+            }
+            mSuspendedEffects.removeItemsAt(index);
+        }
+    }
+}
+
+// must be called with ThreadBase::mLock held
+void AudioFlinger::EffectChain::setEffectSuspendedAll_l(bool suspend)
+{
+    sp<SuspendedEffectDesc> desc;
+
+    int index = mSuspendedEffects.indexOfKey((int)kKeyForSuspendAll);
+    if (suspend) {
+        if (index >= 0) {
+            desc = mSuspendedEffects.valueAt(index);
+        } else {
+            desc = new SuspendedEffectDesc();
+            mSuspendedEffects.add((int)kKeyForSuspendAll, desc);
+            LOGV("setEffectSuspendedAll_l() add entry for 0");
+        }
+        if (desc->mRefCount++ == 0) {
+            Vector< sp<EffectModule> > effects = getSuspendEligibleEffects();
+            for (size_t i = 0; i < effects.size(); i++) {
+                setEffectSuspended_l(&effects[i]->desc().type, true);
+            }
+        }
+    } else {
+        if (index < 0) {
+            return;
+        }
+        desc = mSuspendedEffects.valueAt(index);
+        if (desc->mRefCount <= 0) {
+            LOGW("setEffectSuspendedAll_l() restore refcount should not be 0 %d", desc->mRefCount);
+            desc->mRefCount = 1;
+        }
+        if (--desc->mRefCount == 0) {
+            Vector<const effect_uuid_t *> types;
+            for (size_t i = 0; i < mSuspendedEffects.size(); i++) {
+                if (mSuspendedEffects.keyAt(i) == (int)kKeyForSuspendAll) {
+                    continue;
+                }
+                types.add(&mSuspendedEffects.valueAt(i)->mType);
+            }
+            for (size_t i = 0; i < types.size(); i++) {
+                setEffectSuspended_l(types[i], false);
+            }
+            LOGV("setEffectSuspendedAll_l() remove entry for %08x", mSuspendedEffects.keyAt(index));
+            mSuspendedEffects.removeItem((int)kKeyForSuspendAll);
+        }
+    }
+}
+
+Vector< sp<AudioFlinger::EffectModule> > AudioFlinger::EffectChain::getSuspendEligibleEffects()
+{
+    Vector< sp<EffectModule> > effects;
+    for (size_t i = 0; i < mEffects.size(); i++) {
+        effect_descriptor_t desc = mEffects[i]->desc();
+        // auxiliary effects and vizualizer are never suspended on output mix
+        if ((mSessionId == AUDIO_SESSION_OUTPUT_MIX) && (
+            ((desc.flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY) ||
+             (memcmp(&desc.type, SL_IID_VISUALIZATION, sizeof(effect_uuid_t)) == 0))) {
+            continue;
+        }
+        effects.add(mEffects[i]);
+    }
+    return effects;
+}
+
+sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectIfEnabled(
+                                                            const effect_uuid_t *type)
+{
+    sp<EffectModule> effect;
+    effect = getEffectFromType_l(type);
+    if (effect != 0 && !effect->isEnabled()) {
+        effect.clear();
+    }
+    return effect;
+}
+
+void AudioFlinger::EffectChain::checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+                                                            bool enabled)
+{
+    int index = mSuspendedEffects.indexOfKey(effect->desc().type.timeLow);
+    if (enabled) {
+        if (index < 0) {
+            // if the effect is not suspend check if all effects are suspended
+            index = mSuspendedEffects.indexOfKey((int)kKeyForSuspendAll);
+            if (index < 0) {
+                return;
+            }
+            setEffectSuspended_l(&effect->desc().type, enabled);
+            index = mSuspendedEffects.indexOfKey(effect->desc().type.timeLow);
+        }
+        LOGV("checkSuspendOnEffectEnabled() enable suspending fx %08x",
+             effect->desc().type.timeLow);
+        sp<SuspendedEffectDesc> desc = mSuspendedEffects.valueAt(index);
+        // if effect is requested to suspended but was not yet enabled, supend it now.
+        if (desc->mEffect == 0) {
+            desc->mEffect = effect;
+            effect->setEnabled(false);
+            effect->setSuspended(true);
+        }
+    } else {
+        if (index < 0) {
+            return;
+        }
+        LOGV("checkSuspendOnEffectEnabled() disable restoring fx %08x",
+             effect->desc().type.timeLow);
+        sp<SuspendedEffectDesc> desc = mSuspendedEffects.valueAt(index);
+        desc->mEffect.clear();
+        effect->setSuspended(false);
+    }
+}
+
 #undef LOG_TAG
 #define LOG_TAG "AudioFlinger"
 
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index 7b6215f..791341a 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -165,7 +165,7 @@
                         int *id,
                         int *enabled);
 
-    virtual status_t moveEffects(int session, int srcOutput, int dstOutput);
+    virtual status_t moveEffects(int sessionId, int srcOutput, int dstOutput);
 
     enum hardware_call_state {
         AUDIO_HW_IDLE = 0,
@@ -206,6 +206,8 @@
 
                 uint32_t    getMode() { return mMode; }
 
+                bool        btNrec() { return mBtNrec; }
+
 private:
                             AudioFlinger();
     virtual                 ~AudioFlinger();
@@ -477,14 +479,45 @@
                     // strategy is only meaningful for PlaybackThread which implements this method
                     virtual uint32_t getStrategyForSession_l(int sessionId) { return 0; }
 
+                    // suspend or restore effect according to the type of effect passed. a NULL
+                    // type pointer means suspend all effects in the session
+                    void setEffectSuspended(const effect_uuid_t *type,
+                                            bool suspend,
+                                            int sessionId = AUDIO_SESSION_OUTPUT_MIX);
+                    // check if some effects must be suspended/restored when an effect is enabled
+                    // or disabled
+        virtual     void checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+                                                     bool enabled,
+                                                     int sessionId = AUDIO_SESSION_OUTPUT_MIX);
+
         mutable     Mutex                   mLock;
 
     protected:
 
+                    // entry describing an effect being suspended in mSuspendedSessions keyed vector
+                    class SuspendedSessionDesc : public RefBase {
+                    public:
+                        SuspendedSessionDesc() : mRefCount(0) {}
+
+                        int mRefCount;          // number of active suspend requests
+                        effect_uuid_t mType;    // effect type UUID
+                    };
+
                     void        acquireWakeLock();
                     void        acquireWakeLock_l();
                     void        releaseWakeLock();
                     void        releaseWakeLock_l();
+                    void setEffectSuspended_l(const effect_uuid_t *type,
+                                              bool suspend,
+                                              int sessionId = AUDIO_SESSION_OUTPUT_MIX);
+                    // updated mSuspendedSessions when an effect suspended or restored
+                    void        updateSuspendedSessions_l(const effect_uuid_t *type,
+                                                          bool suspend,
+                                                          int sessionId);
+                    // check if some effects must be suspended when an effect chain is added
+                    void checkSuspendOnAddEffectChain_l(const sp<EffectChain>& chain);
+                    // updated mSuspendedSessions when an effect chain is removed
+                    void updateSuspendedSessionsOnRemoveEffectChain_l(const sp<EffectChain>& chain);
 
         friend class Track;
         friend class TrackBase;
@@ -519,6 +552,9 @@
                     sp<IPowerManager>       mPowerManager;
                     sp<IBinder>             mWakeLockToken;
                     sp<PMDeathRecipient>    mDeathRecipient;
+                    // list of suspended effects per session and per type. The first vector is
+                    // keyed by session ID, the second by type UUID timeLow field
+                    KeyedVector< int, KeyedVector< int, sp<SuspendedSessionDesc> > >  mSuspendedSessions;
     };
 
     // --- PlaybackThread ---
@@ -848,7 +884,7 @@
               void audioConfigChanged_l(int event, int ioHandle, void *param2);
 
               uint32_t nextUniqueId();
-              status_t moveEffectChain_l(int session,
+              status_t moveEffectChain_l(int sessionId,
                                      AudioFlinger::PlaybackThread *srcThread,
                                      AudioFlinger::PlaybackThread *dstThread,
                                      bool reRegister);
@@ -908,6 +944,7 @@
                     bool        setOverflow() { bool tmp = mOverflow; mOverflow = true; return tmp; }
 
                     void        dump(char* buffer, size_t size);
+
         private:
             friend class AudioFlinger;
             friend class RecordThread;
@@ -950,8 +987,6 @@
                 AudioStreamIn* getInput() { return mInput; }
                 virtual audio_stream_t* stream() { return &mInput->stream->common; }
 
-
-                void        setTrack(RecordTrack *recordTrack) { mTrack = recordTrack; }
         virtual status_t    getNextBuffer(AudioBufferProvider::Buffer* buffer);
         virtual void        releaseBuffer(AudioBufferProvider::Buffer* buffer);
         virtual bool        checkForNewParameters_l();
@@ -963,6 +998,7 @@
         virtual status_t addEffectChain_l(const sp<EffectChain>& chain);
         virtual size_t removeEffectChain_l(const sp<EffectChain>& chain);
         virtual uint32_t hasAudioSession(int sessionId);
+                RecordTrack* track();
 
     private:
                 RecordThread();
@@ -1059,6 +1095,7 @@
         int16_t     *outBuffer() { return mConfig.outputCfg.buffer.s16; }
         void        setChain(const wp<EffectChain>& chain) { mChain = chain; }
         void        setThread(const wp<ThreadBase>& thread) { mThread = thread; }
+        wp<ThreadBase>& thread() { return mThread; }
 
         status_t addHandle(sp<EffectHandle>& handle);
         void disconnect(const wp<EffectHandle>& handle);
@@ -1071,6 +1108,10 @@
         status_t         setVolume(uint32_t *left, uint32_t *right, bool controller);
         status_t         setMode(uint32_t mode);
         status_t         stop();
+        void             setSuspended(bool suspended);
+        bool             suspended();
+
+        sp<EffectHandle> controlHandle();
 
         status_t         dump(int fd, const Vector<String16>& args);
 
@@ -1099,6 +1140,7 @@
         uint32_t mMaxDisableWaitCnt;    // maximum grace period before forcing an effect off after
                                         // sending disable command.
         uint32_t mDisableWaitCnt;       // current process() calls count during disable period.
+        bool     mSuspended;            // effect is suspended: temporarily disabled by framework
     };
 
     // The EffectHandle class implements the IEffect interface. It provides resources
@@ -1131,13 +1173,17 @@
 
 
         // Give or take control of effect module
-        void setControl(bool hasControl, bool signal);
+        // - hasControl: true if control is given, false if removed
+        // - signal: true client app should be signaled of change, false otherwise
+        // - enabled: state of the effect when control is passed
+        void setControl(bool hasControl, bool signal, bool enabled);
         void commandExecuted(uint32_t cmdCode,
                              uint32_t cmdSize,
                              void *pCmdData,
                              uint32_t replySize,
                              void *pReplyData);
         void setEnabled(bool enabled);
+        bool enabled() { return mEnabled; }
 
         // Getters
         int id() { return mEffect->id(); }
@@ -1160,6 +1206,8 @@
         uint8_t*            mBuffer;        // pointer to parameter area in shared memory
         int mPriority;                      // client application priority to control the effect
         bool mHasControl;                   // true if this handle is controlling the effect
+        bool mEnabled;                      // cached enable state: needed when the effect is
+                                            // restored after being suspended
     };
 
     // the EffectChain class represents a group of effects associated to one audio session.
@@ -1174,6 +1222,10 @@
         EffectChain(const wp<ThreadBase>& wThread, int sessionId);
         ~EffectChain();
 
+        // special key used for an entry in mSuspendedEffects keyed vector
+        // corresponding to a suspend all request.
+        static const int        kKeyForSuspendAll = 0;
+
         void process_l();
 
         void lock() {
@@ -1191,6 +1243,7 @@
 
         sp<EffectModule> getEffectFromDesc_l(effect_descriptor_t *descriptor);
         sp<EffectModule> getEffectFromId_l(int id);
+        sp<EffectModule> getEffectFromType_l(const effect_uuid_t *type);
         bool setVolume_l(uint32_t *left, uint32_t *right);
         void setDevice_l(uint32_t device);
         void setMode_l(uint32_t mode);
@@ -1221,6 +1274,15 @@
         void setStrategy(uint32_t strategy)
                  { mStrategy = strategy; }
 
+        // suspend effect of the given type
+        void setEffectSuspended_l(const effect_uuid_t *type,
+                                  bool suspend);
+        // suspend all eligible effects
+        void setEffectSuspendedAll_l(bool suspend);
+        // check if effects should be suspend or restored when a given effect is enable or disabled
+        virtual void checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+                                              bool enabled);
+
         status_t dump(int fd, const Vector<String16>& args);
 
     protected:
@@ -1228,6 +1290,21 @@
         EffectChain(const EffectChain&);
         EffectChain& operator =(const EffectChain&);
 
+        class SuspendedEffectDesc : public RefBase {
+        public:
+            SuspendedEffectDesc() : mRefCount(0) {}
+
+            int mRefCount;
+            effect_uuid_t mType;
+            wp<EffectModule> mEffect;
+        };
+
+        // get a list of effect modules to suspend when an effect of the type
+        // passed is enabled.
+        Vector< sp<EffectModule> > getSuspendEligibleEffects();
+        // get an effect module if it is currently enable
+        sp<EffectModule> getEffectIfEnabled(const effect_uuid_t *type);
+
         wp<ThreadBase> mThread;     // parent mixer thread
         Mutex mLock;                // mutex protecting effect list
         Vector<sp<EffectModule> > mEffects; // list of effect modules
@@ -1243,6 +1320,10 @@
         uint32_t mNewLeftVolume;       // new volume on left channel
         uint32_t mNewRightVolume;      // new volume on right channel
         uint32_t mStrategy; // strategy for this effect chain
+        // mSuspendedEffects lists all effect currently suspended in the chain
+        // use effect type UUID timelow field as key. There is no real risk of identical
+        // timeLow fields among effect type UUIDs.
+        KeyedVector< int, sp<SuspendedEffectDesc> > mSuspendedEffects;
     };
 
     struct AudioStreamOut {
@@ -1283,7 +1364,8 @@
 
                 DefaultKeyedVector< pid_t, sp<NotificationClient> >    mNotificationClients;
                 volatile int32_t                    mNextUniqueId;
-                uint32_t mMode;
+                uint32_t                            mMode;
+                bool                                mBtNrec;
 
 };
 
diff --git a/tests/TileBenchmark/res/values/strings.xml b/tests/TileBenchmark/res/values/strings.xml
index 66972ac..c4fd189 100644
--- a/tests/TileBenchmark/res/values/strings.xml
+++ b/tests/TileBenchmark/res/values/strings.xml
@@ -71,8 +71,16 @@
     <string name="frames_per_second">Frames/sec</string>
     <!-- Portion of viewport covered by good tiles [CHAR LIMIT=15] -->
     <string name="viewport_coverage">Coverage</string>
+    <!-- Milliseconds taken to inval, and re-render the page [CHAR LIMIT=15] -->
+    <string name="render_millis">RenderMillis</string>
     <!-- Format string for stat value overlay [CHAR LIMIT=15] -->
     <string name="format_stat">%4.4f</string>
+
+    <!-- Format string for viewport position value overlay [CHAR LIMIT=25] -->
+    <string name="format_view_pos">View:(%1$d,%2$d)-(%3$d,%4$d)</string>
+    <!-- Format string for viewport position value overlay [CHAR LIMIT=25] -->
+    <string name="format_inval_pos">Inval:(%1$d,%2$d)-(%3$d,%4$d)</string>
+
     <!-- Format string for displaying aggregate stats+values (nr of valid tiles,
     etc.) [CHAR LIMIT=20] -->
     <string name="format_stat_name">%1$-20s %2$3d</string>
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
index 36694a7..1eb1c00 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
@@ -83,14 +83,14 @@
         }
     };
 
-    private class LoadFileTask extends AsyncTask<String, Void, TileData[][]> {
+    private class LoadFileTask extends AsyncTask<String, Void, RunData> {
         @Override
-        protected TileData[][] doInBackground(String... params) {
-            TileData[][] data = null;
+        protected RunData doInBackground(String... params) {
+            RunData data = null;
             try {
                 FileInputStream fis = openFileInput(params[0]);
                 ObjectInputStream in = new ObjectInputStream(fis);
-                data = (TileData[][]) in.readObject();
+                data = (RunData) in.readObject();
                 in.close();
             } catch (IOException ex) {
                 ex.printStackTrace();
@@ -101,7 +101,7 @@
         }
 
         @Override
-        protected void onPostExecute(TileData data[][]) {
+        protected void onPostExecute(RunData data) {
             if (data == null) {
                 Toast.makeText(getApplicationContext(),
                         getResources().getString(R.string.error_no_data),
@@ -110,7 +110,7 @@
             }
             mPlaybackView.setData(data);
 
-            mFrameMax = data.length - 1;
+            mFrameMax = data.frames.length - 1;
             mSeekBar.setMax(mFrameMax);
 
             setFrame(null, 0);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
index 35b1563..aad138c 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
@@ -22,10 +22,12 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
-import android.os.Bundle;
+
+import com.test.tilebenchmark.RunData.TileData;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 
 public class PlaybackGraphs {
     private static final int BAR_WIDTH = PlaybackView.TILE_SCALE * 3;
@@ -44,7 +46,7 @@
         return 0.0f;
     }
 
-    private interface MetricGen {
+    protected interface MetricGen {
         public double getValue(TileData[] frame);
 
         public double getMax();
@@ -52,7 +54,7 @@
         public int getLabelId();
     };
 
-    private static MetricGen[] Metrics = new MetricGen[] {
+    protected static MetricGen[] Metrics = new MetricGen[] {
             new MetricGen() {
                 // framerate graph
                 @Override
@@ -99,7 +101,7 @@
             }
     };
 
-    private interface StatGen {
+    protected interface StatGen {
         public double getValue(double sortedValues[]);
 
         public int getLabelId();
@@ -116,7 +118,7 @@
                 + sortedValues[intIndex + 1] * (alpha);
     }
 
-    private static StatGen[] Stats = new StatGen[] {
+    protected static StatGen[] Stats = new StatGen[] {
             new StatGen() {
                 @Override
                 public double getValue(double[] sortedValues) {
@@ -157,21 +159,22 @@
     }
 
     private ArrayList<ShapeDrawable> mShapes = new ArrayList<ShapeDrawable>();
-    private double[][] mStats = new double[Metrics.length][Stats.length];
+    protected double[][] mStats = new double[Metrics.length][Stats.length];
+    protected HashMap<String, Double> mSingleStats;
 
-    public void setData(TileData[][] tileProfilingData) {
+    public void setData(RunData data) {
         mShapes.clear();
-        double metricValues[] = new double[tileProfilingData.length];
+        double metricValues[] = new double[data.frames.length];
 
-        if (tileProfilingData.length == 0) {
+        if (data.frames.length == 0) {
             return;
         }
 
         for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
             // create graph out of rectangles, one per frame
             int lastBar = 0;
-            for (int frameIndex = 0; frameIndex < tileProfilingData.length; frameIndex++) {
-                TileData frame[] = tileProfilingData[frameIndex];
+            for (int frameIndex = 0; frameIndex < data.frames.length; frameIndex++) {
+                TileData frame[] = data.frames[frameIndex];
                 int newBar = (frame[0].top + frame[0].bottom) / 2;
 
                 MetricGen s = Metrics[metricIndex];
@@ -194,9 +197,11 @@
             // store aggregate statistics per metric (median, and similar)
             Arrays.sort(metricValues);
             for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
-                mStats[metricIndex][statIndex] = Stats[statIndex]
-                        .getValue(metricValues);
+                mStats[metricIndex][statIndex] =
+                        Stats[statIndex].getValue(metricValues);
             }
+
+            mSingleStats = data.singleStats;
         }
     }
 
@@ -215,7 +220,7 @@
     }
 
     public void draw(Canvas canvas, ArrayList<ShapeDrawable> shapes,
-            String[] strings, Resources resources) {
+            ArrayList<String> strings, Resources resources) {
         canvas.scale(CANVAS_SCALE, CANVAS_SCALE);
 
         canvas.translate(BAR_WIDTH * Metrics.length, 0);
@@ -238,26 +243,9 @@
                 canvas.drawText(label, xPos, yPos, whiteLabels);
             }
         }
-        for (int stringIndex = 0; stringIndex < strings.length; stringIndex++) {
+        for (int stringIndex = 0; stringIndex < strings.size(); stringIndex++) {
             int yPos = LABELOFFSET + stringIndex * PlaybackView.TILE_SCALE / 2;
-            canvas.drawText(strings[stringIndex], 0, yPos, whiteLabels);
+            canvas.drawText(strings.get(stringIndex), 0, yPos, whiteLabels);
         }
     }
-
-    public Bundle getStatBundle(Resources resources) {
-        Bundle b = new Bundle();
-
-        for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
-            for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
-                String metricLabel = resources.getString(
-                        Metrics[metricIndex].getLabelId());
-                String statLabel = resources.getString(
-                        Stats[statIndex].getLabelId());
-                double value = mStats[metricIndex][statIndex];
-                b.putDouble(metricLabel + " " + statLabel, value);
-            }
-        }
-
-        return b;
-    }
 }
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
index edc8643..5459c1f 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
@@ -30,10 +30,12 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import com.test.tilebenchmark.RunData.TileData;
+
 import java.util.ArrayList;
 
 public class PlaybackView extends View {
-    public static final int TILE_SCALE = 300;
+    public static final int TILE_SCALE = 256;
     private static final int INVAL_FLAG = -2;
     private static final int INVAL_CYCLE = 250;
 
@@ -41,9 +43,9 @@
     private PlaybackGraphs mGraphs;
 
     private ArrayList<ShapeDrawable> mTempShapes = new ArrayList<ShapeDrawable>();
-    private TileData mProfData[][] = null;
+    private RunData mProfData = null;
     private GestureDetector mGestureDetector = null;
-    private String mRenderStrings[] = new String[4];
+    private ArrayList<String> mRenderStrings = new ArrayList<String>();
 
     private class TileDrawable extends ShapeDrawable {
         TileData tile;
@@ -135,17 +137,30 @@
         invalidate(); // may have animations, force redraw
     }
 
+    private String statString(int labelId, int value) {
+        return getResources().getString(R.string.format_stat_name,
+                getResources().getString(labelId), value);
+    }
+    private String tileString(int formatStringId, TileData t) {
+        return getResources().getString(formatStringId,
+                t.left, t.top, t.right, t.bottom);
+    }
+
     public int setFrame(int frame) {
-        if (mProfData == null || mProfData.length == 0) {
+        if (mProfData == null || mProfData.frames.length == 0) {
             return 0;
         }
 
         int readyTiles = 0, unreadyTiles = 0, unplacedTiles = 0, numInvals = 0;
         mTempShapes.clear();
+        mRenderStrings.clear();
 
         // create tile shapes (as they're drawn on bottom)
-        for (TileData t : mProfData[frame]) {
-            if (t.level != INVAL_FLAG && t != mProfData[frame][0]) {
+        for (TileData t : mProfData.frames[frame]) {
+            if (t == mProfData.frames[frame][0]){
+                // viewport 'tile', add coords to render strings
+                mRenderStrings.add(tileString(R.string.format_view_pos, t));
+            } else  if (t.level != INVAL_FLAG) {
                 int colorId;
                 if (t.isReady) {
                     readyTiles++;
@@ -159,14 +174,16 @@
                 }
                 mTempShapes.add(new TileDrawable(t, colorId));
             } else {
+                // inval 'tile', count and add coords to render strings
                 numInvals++;
+                mRenderStrings.add(tileString(R.string.format_inval_pos, t));
             }
         }
 
         // create invalidate shapes (drawn above tiles)
         int invalId = 0;
-        for (TileData t : mProfData[frame]) {
-            if (t.level == INVAL_FLAG && t != mProfData[frame][0]) {
+        for (TileData t : mProfData.frames[frame]) {
+            if (t.level == INVAL_FLAG && t != mProfData.frames[frame][0]) {
                 TileDrawable invalShape = new TileDrawable(t,
                         R.color.inval_region_start);
                 ValueAnimator tileAnimator = ObjectAnimator.ofInt(invalShape,
@@ -186,26 +203,20 @@
             }
         }
 
-        mRenderStrings[0] = getResources().getString(R.string.format_stat_name,
-                getResources().getString(R.string.ready_tiles), readyTiles);
-        mRenderStrings[1] = getResources().getString(R.string.format_stat_name,
-                getResources().getString(R.string.unready_tiles), unreadyTiles);
-        mRenderStrings[2] = getResources().getString(R.string.format_stat_name,
-                getResources().getString(R.string.unplaced_tiles),
-                unplacedTiles);
-        mRenderStrings[3] = getResources().getString(R.string.format_stat_name,
-                getResources().getString(R.string.number_invalidates),
-                numInvals);
+        mRenderStrings.add(statString(R.string.ready_tiles, readyTiles));
+        mRenderStrings.add(statString(R.string.unready_tiles, unreadyTiles));
+        mRenderStrings.add(statString(R.string.unplaced_tiles, unplacedTiles));
+        mRenderStrings.add(statString(R.string.number_invalidates, numInvals));
 
         // draw view rect (using first TileData object, on top)
-        TileDrawable viewShape = new TileDrawable(mProfData[frame][0],
+        TileDrawable viewShape = new TileDrawable(mProfData.frames[frame][0],
                 R.color.view);
         mTempShapes.add(viewShape);
         this.invalidate();
         return frame;
     }
 
-    public void setData(TileData[][] tileProfilingData) {
+    public void setData(RunData tileProfilingData) {
         mProfData = tileProfilingData;
 
         mGraphs.setData(mProfData);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
index 1521807..a63a2f0 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
@@ -51,11 +51,11 @@
 public class ProfileActivity extends Activity {
 
     public interface ProfileCallback {
-        public void profileCallback(TileData data[][]);
+        public void profileCallback(RunData data);
     }
 
     public static final String TEMP_FILENAME = "profile.tiles";
-    private static final int LOAD_TEST_DELAY = 2000; // nr of millis after load,
+    private static final int LOAD_TEST_DELAY = 1000; // nr of millis after load,
                                                      // before test
 
     Button mInspectButton;
@@ -135,6 +135,7 @@
         public void onPageFinished(WebView view, String url) {
             super.onPageFinished(view, url);
             view.requestFocus();
+
             new CountDownTimer(LOAD_TEST_DELAY, LOAD_TEST_DELAY) {
                 @Override
                 public void onTick(long millisUntilFinished) {
@@ -155,10 +156,10 @@
     }
 
     private class StoreFileTask extends
-            AsyncTask<Pair<String, TileData[][]>, Void, Void> {
+            AsyncTask<Pair<String, RunData>, Void, Void> {
 
         @Override
-        protected Void doInBackground(Pair<String, TileData[][]>... params) {
+        protected Void doInBackground(Pair<String, RunData>... params) {
             try {
                 FileOutputStream fos = openFileOutput(params[0].first,
                         Context.MODE_PRIVATE);
@@ -205,10 +206,8 @@
 
     /** auto - automatically scroll. */
     private void startViewProfiling(boolean auto) {
-        if (!auto) {
-            // manual, toggle capture button to indicate capture state to user
-            mCaptureButton.setChecked(true);
-        }
+        // toggle capture button to indicate capture state to user
+        mCaptureButton.setChecked(true);
         mWeb.startScrollTest(mCallback, auto);
         setTestingState(TestingState.START_TESTING);
     }
@@ -227,8 +226,8 @@
         mCallback = new ProfileCallback() {
             @SuppressWarnings("unchecked")
             @Override
-            public void profileCallback(TileData[][] data) {
-                new StoreFileTask().execute(new Pair<String, TileData[][]>(
+            public void profileCallback(RunData data) {
+                new StoreFileTask().execute(new Pair<String, RunData>(
                         TEMP_FILENAME, data));
                 mCaptureButton.setChecked(false);
                 setTestingState(TestingState.STOP_TESTING);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
index d3941be..3fc4665 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
@@ -18,15 +18,19 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.webkit.WebView;
 
 import com.test.tilebenchmark.ProfileActivity.ProfileCallback;
+import com.test.tilebenchmark.RunData.TileData;
 
 public class ProfiledWebView extends WebView {
     private int mSpeed;
 
+    private boolean isTesting = false;
     private boolean isScrolling = false;
     private ProfileCallback mCallback;
+    private long mContentInvalMillis;
 
     public ProfiledWebView(Context context) {
         super(context);
@@ -47,7 +51,7 @@
 
     @Override
     protected void onDraw(android.graphics.Canvas canvas) {
-        if (isScrolling) {
+        if (isTesting && isScrolling) {
             if (canScrollVertically(1)) {
                 scrollBy(0, mSpeed);
             } else {
@@ -60,31 +64,53 @@
 
     /*
      * Called once the page is loaded to start scrolling for evaluating tiles.
-     * If autoScrolling isn't set, stop must be called manually.
+     * If autoScrolling isn't set, stop must be called manually. Before
+     * scrolling, invalidate all content and redraw it, measuring time taken.
      */
     public void startScrollTest(ProfileCallback callback, boolean autoScrolling) {
         isScrolling = autoScrolling;
         mCallback = callback;
-        tileProfilingStart();
+        isTesting = false;
+        mContentInvalMillis = System.currentTimeMillis();
+        registerPageSwapCallback();
+        contentInvalidateAll();
         invalidate();
     }
 
     /*
+     * Called after the manual contentInvalidateAll, after the tiles have all
+     * been redrawn.
+     */
+    @Override
+    protected void pageSwapCallback() {
+        mContentInvalMillis = System.currentTimeMillis() - mContentInvalMillis;
+        super.pageSwapCallback();
+        Log.d("ProfiledWebView", "REDRAW TOOK " + mContentInvalMillis
+                + "millis");
+        isTesting = true;
+        invalidate(); // ensure a redraw so that auto-scrolling can occur
+        tileProfilingStart();
+    }
+
+    /*
      * Called once the page has stopped scrolling
      */
     public void stopScrollTest() {
-        super.tileProfilingStop();
+        tileProfilingStop();
+        isTesting = false;
 
         if (mCallback == null) {
             tileProfilingClear();
             return;
         }
 
-        TileData data[][] = new TileData[super.tileProfilingNumFrames()][];
-        for (int frame = 0; frame < data.length; frame++) {
-            data[frame] = new TileData[
+        RunData data = new RunData(super.tileProfilingNumFrames());
+        data.singleStats.put(getResources().getString(R.string.render_millis),
+                (double)mContentInvalMillis);
+        for (int frame = 0; frame < data.frames.length; frame++) {
+            data.frames[frame] = new TileData[
                     tileProfilingNumTilesInFrame(frame)];
-            for (int tile = 0; tile < data[frame].length; tile++) {
+            for (int tile = 0; tile < data.frames[frame].length; tile++) {
                 int left = tileProfilingGetInt(frame, tile, "left");
                 int top = tileProfilingGetInt(frame, tile, "top");
                 int right = tileProfilingGetInt(frame, tile, "right");
@@ -96,18 +122,18 @@
 
                 float scale = tileProfilingGetFloat(frame, tile, "scale");
 
-                data[frame][tile] = new TileData(left, top, right, bottom,
+                data.frames[frame][tile] = data.new TileData(left, top, right, bottom,
                         isReady, level, scale);
             }
         }
-        super.tileProfilingClear();
+        tileProfilingClear();
 
         mCallback.profileCallback(data);
     }
 
     @Override
     public void loadUrl(String url) {
-        if (!url.startsWith("http://")) {
+        if (!url.startsWith("http://") && !url.startsWith("file://")) {
             url = "http://" + url;
         }
         super.loadUrl(url);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/RunData.java b/tests/TileBenchmark/src/com/test/tilebenchmark/RunData.java
new file mode 100644
index 0000000..2da61cc
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/RunData.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.test.tilebenchmark;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+public class RunData implements Serializable {
+    public TileData[][] frames;
+    public HashMap<String, Double> singleStats = new HashMap<String, Double>();
+
+    public RunData(int frames) {
+        this.frames = new TileData[frames][];
+    }
+
+    public class TileData implements Serializable {
+        public int left, top, right, bottom;
+        public boolean isReady;
+        public int level;
+        public float scale;
+
+        public TileData(int left, int top, int right, int bottom,
+                boolean isReady, int level, float scale) {
+            this.left = left;
+            this.right = right;
+            this.top = top;
+            this.bottom = bottom;
+            this.isReady = isReady;
+            this.level = level;
+            this.scale = scale;
+        }
+
+        public String toString() {
+            return "Tile (" + left + "," + top + ")->("
+                    + right + "," + bottom + ")";
+        }
+    }
+
+}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java b/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java
deleted file mode 100644
index 3e729a6..0000000
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java
+++ /dev/null
@@ -1,42 +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 com.test.tilebenchmark;
-
-import java.io.Serializable;
-
-public class TileData implements Serializable {
-    int left, top, right, bottom;
-    public boolean isReady;
-    public int level;
-    public float scale;
-
-    public TileData(int left, int top, int right, int bottom, boolean isReady,
-            int level, float scale) {
-        this.left = left;
-        this.right = right;
-        this.top = top;
-        this.bottom = bottom;
-        this.isReady = isReady;
-        this.level = level;
-        this.scale = scale;
-    }
-
-    public String toString() {
-        return "Tile (" + left + "," + top + ")->("
-                + right + "," + bottom + ")";
-    }
-}
diff --git a/tests/TileBenchmark/tests/Android.mk b/tests/TileBenchmark/tests/Android.mk
new file mode 100644
index 0000000..8b235ec
--- /dev/null
+++ b/tests/TileBenchmark/tests/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TileBenchmarkTests
+
+LOCAL_INSTRUMENTATION_FOR := TileBenchmark
+
+include $(BUILD_PACKAGE)
diff --git a/tests/TileBenchmark/tests/AndroidManifest.xml b/tests/TileBenchmark/tests/AndroidManifest.xml
new file mode 100644
index 0000000..703b152
--- /dev/null
+++ b/tests/TileBenchmark/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.test.tilebenchmark.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+                     android:targetPackage="com.test.tilebenchmark"
+                     android:label="Tests for WebView Tiles."/>
+</manifest>
diff --git a/tests/TileBenchmark/tests/src/com/test/tilebenchmark/PerformanceTest.java b/tests/TileBenchmark/tests/src/com/test/tilebenchmark/PerformanceTest.java
new file mode 100644
index 0000000..0f02239
--- /dev/null
+++ b/tests/TileBenchmark/tests/src/com/test/tilebenchmark/PerformanceTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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 com.test.tilebenchmark;
+
+import com.test.tilebenchmark.ProfileActivity.ProfileCallback;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Environment;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+public class PerformanceTest extends
+        ActivityInstrumentationTestCase2<ProfileActivity> {
+
+    private class StatAggregator extends PlaybackGraphs {
+        private HashMap<String, Double> mDataMap = new HashMap<String, Double>();
+        private int mCount = 0;
+
+        public void aggregate() {
+            mCount++;
+            Resources resources = mView.getResources();
+            for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
+                for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
+                    String metricLabel = resources.getString(
+                            Metrics[metricIndex].getLabelId());
+                    String statLabel = resources.getString(
+                            Stats[statIndex].getLabelId());
+
+                    String label = metricLabel + " " + statLabel;
+                    double aggVal = mDataMap.containsKey(label) ? mDataMap
+                            .get(label) : 0;
+
+                    aggVal += mStats[metricIndex][statIndex];
+                    mDataMap.put(label, aggVal);
+                }
+            }
+            for (Map.Entry<String, Double> e : mSingleStats.entrySet()) {
+                double aggVal = mDataMap.containsKey(e.getKey())
+                        ? mDataMap.get(e.getKey()) : 0;
+                mDataMap.put(e.getKey(), aggVal + e.getValue());
+            }
+        }
+
+        public Bundle getBundle() {
+            Bundle b = new Bundle();
+            int count = 0 == mCount ? Integer.MAX_VALUE : mCount;
+            for (Map.Entry<String, Double> e : mDataMap.entrySet()) {
+                b.putDouble(e.getKey(), e.getValue() / count);
+            }
+            return b;
+        }
+    }
+
+    ProfileActivity mActivity;
+    ProfiledWebView mView;
+    StatAggregator mStats = new StatAggregator();
+
+    private static final String LOGTAG = "PerformanceTest";
+    private static final String TEST_LOCATION = "webkit/page_cycler";
+    private static final String URL_PREFIX = "file://";
+    private static final String URL_POSTFIX = "/index.html?skip=true";
+    private static final int MAX_ITERATIONS = 4;
+    private static final String TEST_DIRS[] = {
+            "alexa_us"//, "android", "dom", "intl1", "intl2", "moz", "moz2"
+    };
+
+    public PerformanceTest() {
+        super(ProfileActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mActivity = getActivity();
+        mView = (ProfiledWebView) mActivity.findViewById(R.id.web);
+    }
+
+    private boolean loadUrl(final String url) {
+        try {
+            Log.d(LOGTAG, "test starting for url " + url);
+            mActivity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mView.loadUrl(url);
+                }
+            });
+            synchronized (mStats) {
+                mStats.wait();
+            }
+            mStats.aggregate();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+            return false;
+        }
+        return true;
+    }
+
+    private boolean runIteration() {
+        File sdFile = Environment.getExternalStorageDirectory();
+        for (String testDirName : TEST_DIRS) {
+            File testDir = new File(sdFile, TEST_LOCATION + "/" + testDirName);
+            Log.d(LOGTAG, "Testing dir: '" + testDir.getAbsolutePath()
+                    + "', exists=" + testDir.exists());
+            for (File siteDir : testDir.listFiles()) {
+                if (!siteDir.isDirectory())
+                    continue;
+
+                if (!loadUrl(URL_PREFIX + siteDir.getAbsolutePath()
+                        + URL_POSTFIX)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public void testMetrics() {
+        String state = Environment.getExternalStorageState();
+
+        if (!Environment.MEDIA_MOUNTED.equals(state)
+                && !Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+            Log.d(LOGTAG, "ARG Can't access sd card!");
+            // Can't read the SD card, fail and die!
+            getInstrumentation().sendStatus(1, null);
+            return;
+        }
+
+        // use mGraphs as a condition variable between the UI thread and
+        // this(the testing) thread
+        mActivity.setCallback(new ProfileCallback() {
+            @Override
+            public void profileCallback(RunData data) {
+                Log.d(LOGTAG, "test completion callback");
+                mStats.setData(data);
+                synchronized (mStats) {
+                    mStats.notify();
+                }
+            }
+        });
+
+        for (int i = 0; i < MAX_ITERATIONS; i++)
+            if (!runIteration()) {
+                getInstrumentation().sendStatus(1, null);
+                return;
+            }
+        getInstrumentation().sendStatus(0, mStats.getBundle());
+    }
+}