Merge "Import translations. DO NOT MERGE"
diff --git a/api/current.txt b/api/current.txt
index 0030f41..eb6de96 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6772,6 +6772,7 @@
     field public static final java.lang.String CATEGORY_HOME = "android.intent.category.HOME";
     field public static final java.lang.String CATEGORY_INFO = "android.intent.category.INFO";
     field public static final java.lang.String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER";
+    field public static final java.lang.String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
     field public static final java.lang.String CATEGORY_LE_DESK_DOCK = "android.intent.category.LE_DESK_DOCK";
     field public static final java.lang.String CATEGORY_MONKEY = "android.intent.category.MONKEY";
     field public static final java.lang.String CATEGORY_OPENABLE = "android.intent.category.OPENABLE";
@@ -13815,6 +13816,7 @@
     field public static final int RATING_4_STARS = 4; // 0x4
     field public static final int RATING_5_STARS = 5; // 0x5
     field public static final int RATING_HEART = 1; // 0x1
+    field public static final int RATING_NONE = 0; // 0x0
     field public static final int RATING_PERCENTAGE = 6; // 0x6
     field public static final int RATING_THUMB_UP_DOWN = 2; // 0x2
   }
@@ -14455,34 +14457,76 @@
 package android.media.session {
 
   public final class MediaController {
-    ctor public MediaController(android.media.session.MediaSessionToken);
     method public void addCallback(android.media.session.MediaController.Callback);
     method public void addCallback(android.media.session.MediaController.Callback, android.os.Handler);
+    method public static android.media.session.MediaController fromToken(android.media.session.MediaSessionToken);
+    method public android.media.session.TransportController getTransportController();
     method public void removeCallback(android.media.session.MediaController.Callback);
-    method public void sendCommand(java.lang.String, android.os.Bundle);
+    method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
     method public void sendMediaButton(int);
   }
 
   public static abstract class MediaController.Callback {
     ctor public MediaController.Callback();
     method public void onEvent(java.lang.String, android.os.Bundle);
-    method public void onMetadataUpdate(android.os.Bundle);
-    method public void onPlaybackStateChange(int);
     method public void onRouteChanged(android.os.Bundle);
   }
 
+  public final class MediaMetadata implements android.os.Parcelable {
+    method public int describeContents();
+    method public android.graphics.Bitmap getBitmap(java.lang.String);
+    method public long getLong(java.lang.String);
+    method public android.media.Rating getRating(java.lang.String);
+    method public java.lang.String getString(java.lang.String);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+    field public static final java.lang.String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+    field public static final java.lang.String METADATA_KEY_ART = "android.media.metadata.ART";
+    field public static final java.lang.String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+    field public static final java.lang.String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+    field public static final java.lang.String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+    field public static final java.lang.String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+    field public static final java.lang.String METADATA_KEY_DATE = "android.media.metadata.DATE";
+    field public static final java.lang.String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+    field public static final java.lang.String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+    field public static final java.lang.String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+    field public static final java.lang.String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+    field public static final java.lang.String METADATA_KEY_RATING = "android.media.metadata.RATING";
+    field public static final java.lang.String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+    field public static final java.lang.String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+    field public static final java.lang.String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+    field public static final java.lang.String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+    field public static final java.lang.String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+  }
+
+  public static final class MediaMetadata.Builder {
+    ctor public MediaMetadata.Builder();
+    ctor public MediaMetadata.Builder(android.media.session.MediaMetadata);
+    method public android.media.session.MediaMetadata build();
+    method public android.media.session.MediaMetadata.Builder putBitmap(java.lang.String, android.graphics.Bitmap);
+    method public android.media.session.MediaMetadata.Builder putLong(java.lang.String, long);
+    method public android.media.session.MediaMetadata.Builder putRating(java.lang.String, android.media.Rating);
+    method public android.media.session.MediaMetadata.Builder putString(java.lang.String, java.lang.String);
+  }
+
   public final class MediaSession {
     method public void addCallback(android.media.session.MediaSession.Callback);
     method public void addCallback(android.media.session.MediaSession.Callback, android.os.Handler);
     method public android.media.session.MediaSessionToken getSessionToken();
+    method public android.media.session.TransportPerformer getTransportPerformer();
+    method public void publish();
     method public void release();
     method public void removeCallback(android.media.session.MediaSession.Callback);
-    method public void setPlaybackState(int);
+    method public void sendEvent(java.lang.String, android.os.Bundle);
+    method public android.media.session.TransportPerformer setTransportPerformerEnabled();
   }
 
   public static abstract class MediaSession.Callback {
     ctor public MediaSession.Callback();
-    method public void onCommand(java.lang.String, android.os.Bundle);
+    method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
     method public void onMediaButton(android.content.Intent);
     method public void onRequestRouteChange(android.os.Bundle);
   }
@@ -14498,6 +14542,137 @@
     field public static final android.os.Parcelable.Creator CREATOR;
   }
 
+  public final class PlaybackState implements android.os.Parcelable {
+    ctor public PlaybackState();
+    ctor public PlaybackState(android.media.session.PlaybackState);
+    method public int describeContents();
+    method public long getActions();
+    method public long getBufferPosition();
+    method public java.lang.String getErrorMessage();
+    method public long getPosition();
+    method public float getSpeed();
+    method public int getState();
+    method public void setActions(long);
+    method public void setBufferPosition(long);
+    method public void setErrorMessage(java.lang.String);
+    method public void setPosition(long);
+    method public void setSpeed(float);
+    method public void setState(int);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final long ACTION_FASTFORWARD = 64L; // 0x40L
+    field public static final long ACTION_NEXT_ITEM = 32L; // 0x20L
+    field public static final long ACTION_PAUSE = 2L; // 0x2L
+    field public static final long ACTION_PLAY = 4L; // 0x4L
+    field public static final long ACTION_PREVIOUS_ITEM = 16L; // 0x10L
+    field public static final long ACTION_RATING = 128L; // 0x80L
+    field public static final long ACTION_REWIND = 8L; // 0x8L
+    field public static final long ACTION_SEEK_TO = 256L; // 0x100L
+    field public static final long ACTION_STOP = 1L; // 0x1L
+    field public static final android.os.Parcelable.Creator CREATOR;
+    field public static final int PLAYSTATE_BUFFERING = 6; // 0x6
+    field public static final int PLAYSTATE_ERROR = 7; // 0x7
+    field public static final int PLAYSTATE_FAST_FORWARDING = 4; // 0x4
+    field public static final int PLAYSTATE_NONE = 0; // 0x0
+    field public static final int PLAYSTATE_PAUSED = 2; // 0x2
+    field public static final int PLAYSTATE_PLAYING = 3; // 0x3
+    field public static final int PLAYSTATE_REWINDING = 5; // 0x5
+    field public static final int PLAYSTATE_STOPPED = 1; // 0x1
+  }
+
+  public final class RouteInterface {
+    method public void addListener(android.media.session.RouteInterface.EventListener);
+    method public void addListener(android.media.session.RouteInterface.EventListener, android.os.Handler);
+    method public void removeListener(android.media.session.RouteInterface.EventListener);
+    method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+  }
+
+  public static abstract class RouteInterface.EventListener {
+    ctor public RouteInterface.EventListener();
+    method public abstract void onEvent(java.lang.String, android.os.Bundle);
+  }
+
+  public static abstract class RouteInterface.Stub {
+    ctor public RouteInterface.Stub();
+    method public abstract java.lang.String getName();
+    method public abstract void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+    method public final void sendEvent(android.media.session.MediaSession, java.lang.String, android.os.Bundle);
+  }
+
+  public final class RouteTransportControls {
+    method public void addListener(android.media.session.RouteTransportControls.Listener);
+    method public void addListener(android.media.session.RouteTransportControls.Listener, android.os.Handler);
+    method public void fastForward(float);
+    method public static android.media.session.RouteTransportControls from(android.media.session.MediaController);
+    method public void getCapabilities(android.os.ResultReceiver);
+    method public void getCurrentPosition(android.os.ResultReceiver);
+    method public void pause();
+    method public void play();
+    method public void removeListener(android.media.session.RouteTransportControls.Listener);
+    field public static final java.lang.String NAME = "android.media.session.RouteTransportControls";
+  }
+
+  public static abstract class RouteTransportControls.Listener {
+    ctor public RouteTransportControls.Listener();
+    method public void onMetadataUpdate(android.os.Bundle);
+    method public void onPlaybackStateChange(int);
+  }
+
+  public static abstract class RouteTransportControls.Stub extends android.media.session.RouteInterface.Stub {
+    ctor public RouteTransportControls.Stub(android.media.session.MediaSession);
+    method public void fastForward(float);
+    method public long getCapabilities();
+    method public long getCurrentPosition();
+    method public java.lang.String getName();
+    method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+    method public final void updatePlaybackState(int);
+  }
+
+  public final class TransportController {
+    method public void addStateListener(android.media.session.TransportController.TransportStateListener);
+    method public void addStateListener(android.media.session.TransportController.TransportStateListener, android.os.Handler);
+    method public void fastForward();
+    method public android.media.session.MediaMetadata getMetadata();
+    method public android.media.session.PlaybackState getPlaybackState();
+    method public int getRatingType();
+    method public void next();
+    method public void pause();
+    method public void play();
+    method public void previous();
+    method public void rate(android.media.Rating);
+    method public void removeStateListener(android.media.session.TransportController.TransportStateListener);
+    method public void rewind();
+    method public void seekTo(long);
+    method public void stop();
+  }
+
+  public static abstract class TransportController.TransportStateListener {
+    ctor public TransportController.TransportStateListener();
+    method public void onMetadataChanged(android.media.session.MediaMetadata);
+    method public void onPlaybackStateChanged(android.media.session.PlaybackState);
+  }
+
+  public final class TransportPerformer {
+    method public void addListener(android.media.session.TransportPerformer.Listener);
+    method public void addListener(android.media.session.TransportPerformer.Listener, android.os.Handler);
+    method public void removeListener(android.media.session.TransportPerformer.Listener);
+    method public final void setMetadata(android.media.session.MediaMetadata);
+    method public final void setPlaybackState(android.media.session.PlaybackState);
+  }
+
+  public static abstract class TransportPerformer.Listener {
+    ctor public TransportPerformer.Listener();
+    method public void onFastForward();
+    method public void onNext();
+    method public void onPause();
+    method public void onPlay();
+    method public void onPrevious();
+    method public void onRate(android.media.Rating);
+    method public void onRewind();
+    method public void onRouteFocusChange(int);
+    method public void onSeekTo(long);
+    method public void onStop();
+  }
+
 }
 
 package android.mtp {
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index f0b7ca8..96479e2 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2768,6 +2768,12 @@
     @SdkConstant(SdkConstantType.INTENT_CATEGORY)
     public static final String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER";
     /**
+     * Indicates an activity optimized for Leanback mode, and that should
+     * be displayed in the Leanback launcher.
+     */
+    @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+    public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
+    /**
      * Provides information about the package it is in; typically used if
      * a package does not contain a {@link #CATEGORY_LAUNCHER} to provide
      * a front-door to the user without having to be shown in the all apps list.
diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java
index 95faa77..ec44661 100644
--- a/core/java/android/net/EthernetDataTracker.java
+++ b/core/java/android/net/EthernetDataTracker.java
@@ -418,4 +418,9 @@
     public void supplyMessenger(Messenger messenger) {
         // not supported on this network
     }
+
+    @Override
+    public String getNetworkInterfaceName() {
+        return mIface;
+    }
 }
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index 6650fca..5d55143 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -21,11 +21,11 @@
 /**
  * Class that operates the vibrator on the device.
  * <p>
- * If your process exits, any vibration you started with will stop.
+ * If your process exits, any vibration you started will stop.
  * </p>
  *
  * To obtain an instance of the system vibrator, call
- * {@link Context#getSystemService} with {@link Context#VIBRATOR_SERVICE} as argument.
+ * {@link Context#getSystemService} with {@link Context#VIBRATOR_SERVICE} as the argument.
  */
 public abstract class Vibrator {
     /**
diff --git a/core/java/android/view/ContextThemeWrapper.java b/core/java/android/view/ContextThemeWrapper.java
index 1de9c35..0afbde9 100644
--- a/core/java/android/view/ContextThemeWrapper.java
+++ b/core/java/android/view/ContextThemeWrapper.java
@@ -26,7 +26,6 @@
  * wrapped context. 
  */
 public class ContextThemeWrapper extends ContextWrapper {
-    private Context mBase;
     private int mThemeResource;
     private Resources.Theme mTheme;
     private LayoutInflater mInflater;
@@ -39,13 +38,11 @@
     
     public ContextThemeWrapper(Context base, int themeres) {
         super(base);
-        mBase = base;
         mThemeResource = themeres;
     }
 
     @Override protected void attachBaseContext(Context newBase) {
         super.attachBaseContext(newBase);
-        mBase = newBase;
     }
 
     /**
@@ -109,11 +106,11 @@
     @Override public Object getSystemService(String name) {
         if (LAYOUT_INFLATER_SERVICE.equals(name)) {
             if (mInflater == null) {
-                mInflater = LayoutInflater.from(mBase).cloneInContext(this);
+                mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
             }
             return mInflater;
         }
-        return mBase.getSystemService(name);
+        return getBaseContext().getSystemService(name);
     }
     
     /**
@@ -135,7 +132,7 @@
         final boolean first = mTheme == null;
         if (first) {
             mTheme = getResources().newTheme();
-            Resources.Theme theme = mBase.getTheme();
+            Resources.Theme theme = getBaseContext().getTheme();
             if (theme != null) {
                 mTheme.setTo(theme);
             }
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 97ea7d8..4734712 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -41,11 +41,11 @@
             out List<IBinder> notificationKeys, out List<StatusBarNotification> notifications,
             out int[] switches, out List<IBinder> binders);
     void onPanelRevealed();
-    void onNotificationClick(String pkg, String tag, int id);
+    void onNotificationClick(String pkg, String tag, int id, int userId);
     void onNotificationError(String pkg, String tag, int id,
-            int uid, int initialPid, String message);
-    void onClearAllNotifications();
-    void onNotificationClear(String pkg, String tag, int id);
+            int uid, int initialPid, String message, int userId);
+    void onClearAllNotifications(int userId);
+    void onNotificationClear(String pkg, String tag, int id, int userId);
     void setSystemUiVisibility(int vis, int mask);
     void setHardKeyboardEnabled(boolean enabled);
     void toggleRecentApps();
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index e51345c..45ca7fc 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -75,7 +75,7 @@
     boolean switchToNextInputMethod(in IBinder token, boolean onlyCurrentIme);
     boolean shouldOfferSwitchingToNextInputMethod(in IBinder token);
     boolean setInputMethodEnabled(String id, boolean enabled);
-    oneway void setAdditionalInputMethodSubtypes(String id, in InputMethodSubtype[] subtypes);
+    void setAdditionalInputMethodSubtypes(String id, in InputMethodSubtype[] subtypes);
     int getInputMethodWindowVisibleHeight();
     oneway void notifyTextCommitted();
 }
diff --git a/core/java/com/android/internal/widget/SwipeDismissLayout.java b/core/java/com/android/internal/widget/SwipeDismissLayout.java
index cc8ce2c..467d42e 100644
--- a/core/java/com/android/internal/widget/SwipeDismissLayout.java
+++ b/core/java/com/android/internal/widget/SwipeDismissLayout.java
@@ -126,6 +126,20 @@
                 mVelocityTracker.addMovement(ev);
                 break;
 
+            case MotionEvent.ACTION_POINTER_DOWN:
+                int actionIndex = ev.getActionIndex();
+                mActiveTouchId = ev.getPointerId(actionIndex);
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                actionIndex = ev.getActionIndex();
+                int pointerId = ev.getPointerId(actionIndex);
+                if (pointerId == mActiveTouchId) {
+                    // This was our active pointer going up. Choose a new active pointer.
+                    int newActionIndex = actionIndex == 0 ? 1 : 0;
+                    mActiveTouchId = ev.getPointerId(newActionIndex);
+                }
+                break;
+
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
                 resetMembers();
@@ -137,6 +151,11 @@
                 }
 
                 int pointerIndex = ev.findPointerIndex(mActiveTouchId);
+                if (pointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointer index: ignoring.");
+                    mDiscardIntercept = true;
+                    break;
+                }
                 float dx = ev.getRawX() - mDownX;
                 float x = ev.getX(pointerIndex);
                 float y = ev.getY(pointerIndex);
@@ -228,11 +247,11 @@
     }
 
     private void updateDismiss(MotionEvent ev) {
+        float deltaX = ev.getRawX() - mDownX;
         if (!mDismissed) {
             mVelocityTracker.addMovement(ev);
             mVelocityTracker.computeCurrentVelocity(1000);
 
-            float deltaX = ev.getRawX() - mDownX;
             float velocityX = mVelocityTracker.getXVelocity();
             float absVelocityX = Math.abs(velocityX);
             float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
@@ -247,6 +266,13 @@
                 mDismissed = true;
             }
         }
+        // Check if the user tried to undo this.
+        if (mDismissed && mSwiping) {
+            // Check if the user's finger is actually back
+            if (deltaX < getWidth() / 2) {
+                mDismissed = false;
+            }
+        }
     }
 
     /**
diff --git a/core/jni/android_view_DisplayList.cpp b/core/jni/android_view_DisplayList.cpp
index c8952c1..4a6346e 100644
--- a/core/jni/android_view_DisplayList.cpp
+++ b/core/jni/android_view_DisplayList.cpp
@@ -43,7 +43,7 @@
 
 static void android_view_DisplayList_setDisplayListName(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jstring name) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     if (name != NULL) {
         const char* textArray = env->GetStringUTFChars(name, NULL);
         displayList->setName(textArray);
@@ -53,19 +53,19 @@
 
 static void android_view_DisplayList_output(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     displayList->output();
 }
 
 static jlong android_view_DisplayList_create(JNIEnv* env, jobject clazz) {
-    DisplayList* displayList = new DisplayList();
+    RenderNode* displayList = new RenderNode();
     return reinterpret_cast<jlong>(displayList);
 }
 
 static void android_view_DisplayList_destroyDisplayList(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    DisplayList::destroyDisplayListDeferred(displayList);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    RenderNode::destroyDisplayListDeferred(displayList);
 }
 
 // ----------------------------------------------------------------------------
@@ -74,304 +74,303 @@
 
 static void android_view_DisplayList_setCaching(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean caching) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setCaching(caching);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setCaching(caching);
 }
 
 static void android_view_DisplayList_setStaticMatrix(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jlong matrixPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     SkMatrix* matrix = reinterpret_cast<SkMatrix*>(matrixPtr);
-    displayList->setStaticMatrix(matrix);
+    displayList->properties().setStaticMatrix(matrix);
 }
 
 static void android_view_DisplayList_setAnimationMatrix(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jlong matrixPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     SkMatrix* matrix = reinterpret_cast<SkMatrix*>(matrixPtr);
-    displayList->setAnimationMatrix(matrix);
+    displayList->properties().setAnimationMatrix(matrix);
 }
 
 static void android_view_DisplayList_setClipToBounds(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean clipToBounds) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setClipToBounds(clipToBounds);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setClipToBounds(clipToBounds);
 }
 
 static void android_view_DisplayList_setIsolatedZVolume(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean shouldIsolate) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setIsolatedZVolume(shouldIsolate);
+    // No-op, TODO: Remove Java usage of this method
 }
 
 static void android_view_DisplayList_setProjectBackwards(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean shouldProject) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setProjectBackwards(shouldProject);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setProjectBackwards(shouldProject);
 }
 
 static void android_view_DisplayList_setProjectionReceiver(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean shouldRecieve) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setProjectionReceiver(shouldRecieve);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setProjectionReceiver(shouldRecieve);
 }
 
 static void android_view_DisplayList_setOutline(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jlong outlinePathPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     SkPath* outline = reinterpret_cast<SkPath*>(outlinePathPtr);
-    displayList->setOutline(outline);
+    displayList->properties().setOutline(outline);
 }
 
 static void android_view_DisplayList_setClipToOutline(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean clipToOutline) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setClipToOutline(clipToOutline);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setClipToOutline(clipToOutline);
 }
 
 static void android_view_DisplayList_setCastsShadow(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean castsShadow) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setCastsShadow(castsShadow);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setCastsShadow(castsShadow);
 }
 
 static void android_view_DisplayList_setUsesGlobalCamera(JNIEnv* env,
         jobject clazz, jlong displayListPtr, jboolean usesGlobalCamera) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setUsesGlobalCamera(usesGlobalCamera);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setUsesGlobalCamera(usesGlobalCamera);
 }
 
 static void android_view_DisplayList_setAlpha(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float alpha) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setAlpha(alpha);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setAlpha(alpha);
 }
 
 static void android_view_DisplayList_setHasOverlappingRendering(JNIEnv* env,
         jobject clazz, jlong displayListPtr, bool hasOverlappingRendering) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setHasOverlappingRendering(hasOverlappingRendering);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setHasOverlappingRendering(hasOverlappingRendering);
 }
 
 static void android_view_DisplayList_setTranslationX(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float tx) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setTranslationX(tx);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setTranslationX(tx);
 }
 
 static void android_view_DisplayList_setTranslationY(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float ty) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setTranslationY(ty);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setTranslationY(ty);
 }
 
 static void android_view_DisplayList_setTranslationZ(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float tz) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setTranslationZ(tz);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setTranslationZ(tz);
 }
 
 static void android_view_DisplayList_setRotation(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float rotation) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setRotation(rotation);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setRotation(rotation);
 }
 
 static void android_view_DisplayList_setRotationX(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float rx) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setRotationX(rx);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setRotationX(rx);
 }
 
 static void android_view_DisplayList_setRotationY(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float ry) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setRotationY(ry);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setRotationY(ry);
 }
 
 static void android_view_DisplayList_setScaleX(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float sx) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setScaleX(sx);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setScaleX(sx);
 }
 
 static void android_view_DisplayList_setScaleY(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float sy) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setScaleY(sy);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setScaleY(sy);
 }
 
 static void android_view_DisplayList_setTransformationInfo(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float alpha,
         float translationX, float translationY, float translationZ,
         float rotation, float rotationX, float rotationY, float scaleX, float scaleY) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setAlpha(alpha);
-    displayList->setTranslationX(translationX);
-    displayList->setTranslationY(translationY);
-    displayList->setTranslationZ(translationZ);
-    displayList->setRotation(rotation);
-    displayList->setRotationX(rotationX);
-    displayList->setRotationY(rotationY);
-    displayList->setScaleX(scaleX);
-    displayList->setScaleY(scaleY);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setAlpha(alpha);
+    displayList->properties().setTranslationX(translationX);
+    displayList->properties().setTranslationY(translationY);
+    displayList->properties().setTranslationZ(translationZ);
+    displayList->properties().setRotation(rotation);
+    displayList->properties().setRotationX(rotationX);
+    displayList->properties().setRotationY(rotationY);
+    displayList->properties().setScaleX(scaleX);
+    displayList->properties().setScaleY(scaleY);
 }
 
 static void android_view_DisplayList_setPivotX(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float px) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setPivotX(px);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setPivotX(px);
 }
 
 static void android_view_DisplayList_setPivotY(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float py) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setPivotY(py);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setPivotY(py);
 }
 
 static void android_view_DisplayList_setCameraDistance(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float distance) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setCameraDistance(distance);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setCameraDistance(distance);
 }
 
 static void android_view_DisplayList_setLeft(JNIEnv* env,
         jobject clazz, jlong displayListPtr, int left) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setLeft(left);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setLeft(left);
 }
 
 static void android_view_DisplayList_setTop(JNIEnv* env,
         jobject clazz, jlong displayListPtr, int top) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setTop(top);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setTop(top);
 }
 
 static void android_view_DisplayList_setRight(JNIEnv* env,
         jobject clazz, jlong displayListPtr, int right) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setRight(right);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setRight(right);
 }
 
 static void android_view_DisplayList_setBottom(JNIEnv* env,
         jobject clazz, jlong displayListPtr, int bottom) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setBottom(bottom);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setBottom(bottom);
 }
 
 static void android_view_DisplayList_setLeftTopRightBottom(JNIEnv* env,
         jobject clazz, jlong displayListPtr, int left, int top,
         int right, int bottom) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->setLeftTopRightBottom(left, top, right, bottom);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().setLeftTopRightBottom(left, top, right, bottom);
 }
 
 static void android_view_DisplayList_offsetLeftAndRight(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float offset) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->offsetLeftRight(offset);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().offsetLeftRight(offset);
 }
 
 static void android_view_DisplayList_offsetTopAndBottom(JNIEnv* env,
         jobject clazz, jlong displayListPtr, float offset) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    displayList->offsetTopBottom(offset);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    displayList->properties().offsetTopBottom(offset);
 }
 
 static jboolean android_view_DisplayList_hasOverlappingRendering(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->hasOverlappingRendering();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().hasOverlappingRendering();
 }
 
 static jfloat android_view_DisplayList_getAlpha(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getAlpha();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getAlpha();
 }
 
 static jfloat android_view_DisplayList_getLeft(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getLeft();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getLeft();
 }
 
 static jfloat android_view_DisplayList_getTop(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getTop();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getTop();
 }
 
 static jfloat android_view_DisplayList_getRight(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getRight();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getRight();
 }
 
 static jfloat android_view_DisplayList_getBottom(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getBottom();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getBottom();
 }
 
 static jfloat android_view_DisplayList_getCameraDistance(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getCameraDistance();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getCameraDistance();
 }
 
 static jfloat android_view_DisplayList_getScaleX(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getScaleX();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getScaleX();
 }
 
 static jfloat android_view_DisplayList_getScaleY(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getScaleY();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getScaleY();
 }
 
 static jfloat android_view_DisplayList_getTranslationX(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getTranslationX();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getTranslationX();
 }
 
 static jfloat android_view_DisplayList_getTranslationY(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getTranslationY();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getTranslationY();
 }
 
 static jfloat android_view_DisplayList_getRotation(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getRotation();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getRotation();
 }
 
 static jfloat android_view_DisplayList_getRotationX(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getRotationX();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getRotationX();
 }
 
 static jfloat android_view_DisplayList_getRotationY(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getRotationY();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getRotationY();
 }
 
 static jfloat android_view_DisplayList_getPivotX(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getPivotX();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getPivotX();
 }
 
 static jfloat android_view_DisplayList_getPivotY(JNIEnv* env,
         jobject clazz, jlong displayListPtr) {
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
-    return displayList->getPivotY();
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
+    return displayList->properties().getPivotY();
 }
 
 #endif // USE_OPENGL_RENDERER
diff --git a/core/jni/android_view_GLES20Canvas.cpp b/core/jni/android_view_GLES20Canvas.cpp
index a4e6679..aa6a035 100644
--- a/core/jni/android_view_GLES20Canvas.cpp
+++ b/core/jni/android_view_GLES20Canvas.cpp
@@ -879,7 +879,7 @@
         jobject clazz, jlong rendererPtr, jlong displayListPtr,
         jobject dirty, jint flags) {
     OpenGLRenderer* renderer = reinterpret_cast<OpenGLRenderer*>(rendererPtr);
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     android::uirenderer::Rect bounds;
     status_t status = renderer->drawDisplayList(displayList, bounds, flags);
     if (status != DrawGlInfo::kStatusDone && dirty != NULL) {
@@ -975,7 +975,7 @@
 android_app_ActivityThread_dumpGraphics(JNIEnv* env, jobject clazz, jobject javaFileDescriptor) {
 #ifdef USE_OPENGL_RENDERER
     int fd = jniGetFDFromFileDescriptor(env, javaFileDescriptor);
-    android::uirenderer::DisplayList::outputLogBuffer(fd);
+    android::uirenderer::RenderNode::outputLogBuffer(fd);
 #endif // USE_OPENGL_RENDERER
 }
 
diff --git a/core/jni/android_view_GLRenderer.cpp b/core/jni/android_view_GLRenderer.cpp
index b7e795e..228a92e 100644
--- a/core/jni/android_view_GLRenderer.cpp
+++ b/core/jni/android_view_GLRenderer.cpp
@@ -143,7 +143,7 @@
 static void android_view_GLRenderer_setDisplayListData(JNIEnv* env, jobject clazz,
         jlong displayListPtr, jlong newDataPtr) {
     using namespace android::uirenderer;
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     DisplayListData* newData = reinterpret_cast<DisplayListData*>(newDataPtr);
     displayList->setData(newData);
 }
diff --git a/core/jni/android_view_HardwareLayer.cpp b/core/jni/android_view_HardwareLayer.cpp
index 5b21e94..ad2e9ff 100644
--- a/core/jni/android_view_HardwareLayer.cpp
+++ b/core/jni/android_view_HardwareLayer.cpp
@@ -120,7 +120,7 @@
         jlong layerUpdaterPtr, jlong displayListPtr,
         jint left, jint top, jint right, jint bottom) {
     DeferredLayerUpdater* layer = reinterpret_cast<DeferredLayerUpdater*>(layerUpdaterPtr);
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     layer->setDisplayList(displayList, left, top, right, bottom);
 }
 
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index 2b20758..28cee4b 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -106,7 +106,7 @@
 static void android_view_ThreadedRenderer_setDisplayListData(JNIEnv* env, jobject clazz,
         jlong proxyPtr, jlong displayListPtr, jlong newDataPtr) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     DisplayListData* newData = reinterpret_cast<DisplayListData*>(newDataPtr);
     proxy->setDisplayListData(displayList, newData);
 }
@@ -115,7 +115,7 @@
         jlong proxyPtr, jlong displayListPtr, jint dirtyLeft, jint dirtyTop,
         jint dirtyRight, jint dirtyBottom) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    DisplayList* displayList = reinterpret_cast<DisplayList*>(displayListPtr);
+    RenderNode* displayList = reinterpret_cast<RenderNode*>(displayListPtr);
     proxy->drawDisplayList(displayList, dirtyLeft, dirtyTop, dirtyRight, dirtyBottom);
 }
 
diff --git a/docs/html/google/auth/api-client.jd b/docs/html/google/auth/api-client.jd
index fda3310..402a95f 100644
--- a/docs/html/google/auth/api-client.jd
+++ b/docs/html/google/auth/api-client.jd
@@ -112,7 +112,7 @@
 href="{@docRoot}reference/com/google/android/gms/common/api/GoogleApiClient.ConnectionCallbacks.html"
 >{@code ConnectionCallbacks}</a> and <a
 href="{@docRoot}reference/com/google/android/gms/common/api/GoogleApiClient.OnConnectionFailedListener.html"
->{@code onConnectionFailedListener}</a>. These interfaces receive callbacks in
+>{@code OnConnectionFailedListener}</a>. These interfaces receive callbacks in
 response to the asynchronous <a
 href="{@docRoot}reference/com/google/android/gms/common/api/GoogleApiClient.html#connect()"
 >{@code connect()}</a> method when the connection to Google Play services
@@ -512,7 +512,7 @@
     new GetFileTask().execute(filename);
 }
 
-private class GetFileTask extends AsyncTask<String, Void, Void> {
+private class GetFileTask extends AsyncTask&lt;String, Void, Void> {
     protected void doInBackground(String filename) {
         Query query = new Query.Builder()
                 .addFilter(Filters.eq(SearchableField.TITLE, filename))
diff --git a/docs/html/guide/topics/ui/controls/button.jd b/docs/html/guide/topics/ui/controls/button.jd
index 02597c8..b52c3e9 100644
--- a/docs/html/guide/topics/ui/controls/button.jd
+++ b/docs/html/guide/topics/ui/controls/button.jd
@@ -113,7 +113,7 @@
 
 <h3 id="ClickListener">Using an OnClickListener</h3>
 
-<p>You can also declare the click event handler pragmatically rather than in an XML layout. This
+<p>You can also declare the click event handler programmatically rather than in an XML layout. This
 might be necessary if you instantiate the {@link android.widget.Button} at runtime or you need to
 declare the click behavior in a {@link android.app.Fragment} subclass.</p>
 
diff --git a/docs/html/guide/topics/ui/controls/togglebutton.jd b/docs/html/guide/topics/ui/controls/togglebutton.jd
index c57b510..09af516 100644
--- a/docs/html/guide/topics/ui/controls/togglebutton.jd
+++ b/docs/html/guide/topics/ui/controls/togglebutton.jd
@@ -99,7 +99,7 @@
 
 <h3 id="ClickListener">Using an OnCheckedChangeListener</h3>
 
-<p>You can also declare a click event handler pragmatically rather than in an XML layout. This
+<p>You can also declare a click event handler programmatically rather than in an XML layout. This
 might be necessary if you instantiate the {@link android.widget.ToggleButton} or {@link
 android.widget.Switch} at runtime or you need to
 declare the click behavior in a {@link android.app.Fragment} subclass.</p>
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 2cc7a84..eeff4c0 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -38,6 +38,7 @@
 		Program.cpp \
 		ProgramCache.cpp \
 		RenderBufferCache.cpp \
+		RenderProperties.cpp \
 		ResourceCache.cpp \
 		ShadowTessellator.cpp \
 		SkiaShader.cpp \
diff --git a/libs/hwui/Caches.cpp b/libs/hwui/Caches.cpp
index 69d3328..2dfc873 100644
--- a/libs/hwui/Caches.cpp
+++ b/libs/hwui/Caches.cpp
@@ -315,7 +315,7 @@
     pathCache.clearGarbage();
     patchCache.clearGarbage();
 
-    Vector<DisplayList*> displayLists;
+    Vector<RenderNode*> displayLists;
     Vector<Layer*> layers;
 
     { // scope for the lock
@@ -328,7 +328,7 @@
 
     size_t count = displayLists.size();
     for (size_t i = 0; i < count; i++) {
-        DisplayList* displayList = displayLists.itemAt(i);
+        RenderNode* displayList = displayLists.itemAt(i);
         delete displayList;
     }
 
@@ -345,7 +345,7 @@
     mLayerGarbage.push(layer);
 }
 
-void Caches::deleteDisplayListDeferred(DisplayList* displayList) {
+void Caches::deleteDisplayListDeferred(RenderNode* displayList) {
     Mutex::Autolock _l(mGarbageLock);
     mDisplayListGarbage.push(displayList);
 }
diff --git a/libs/hwui/Caches.h b/libs/hwui/Caches.h
index 6f3d8fb..50c5fef 100644
--- a/libs/hwui/Caches.h
+++ b/libs/hwui/Caches.h
@@ -102,7 +102,7 @@
 // Caches
 ///////////////////////////////////////////////////////////////////////////////
 
-class DisplayList;
+class RenderNode;
 
 class ANDROID_API Caches: public Singleton<Caches> {
     Caches();
@@ -169,7 +169,7 @@
     /*
      * Can be used to delete a display list from a non EGL thread.
      */
-    void deleteDisplayListDeferred(DisplayList* layer);
+    void deleteDisplayListDeferred(RenderNode* layer);
 
     /**
      * Binds the VBO used to render simple textured quads.
@@ -420,7 +420,7 @@
 
     mutable Mutex mGarbageLock;
     Vector<Layer*> mLayerGarbage;
-    Vector<DisplayList*> mDisplayListGarbage;
+    Vector<RenderNode*> mDisplayListGarbage;
 
     DebugLevel mDebugLevel;
     bool mInitialized;
diff --git a/libs/hwui/DeferredLayerUpdater.cpp b/libs/hwui/DeferredLayerUpdater.cpp
index 7a2e288..7a83967 100644
--- a/libs/hwui/DeferredLayerUpdater.cpp
+++ b/libs/hwui/DeferredLayerUpdater.cpp
@@ -54,7 +54,7 @@
     SkRefCnt_SafeAssign(mColorFilter, colorFilter);
 }
 
-void DeferredLayerUpdater::setDisplayList(DisplayList* displayList,
+void DeferredLayerUpdater::setDisplayList(RenderNode* displayList,
         int left, int top, int right, int bottom) {
     mDisplayList = displayList;
     if (mDirtyRect.isEmpty()) {
diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h
index 65f225c..d124cde 100644
--- a/libs/hwui/DeferredLayerUpdater.h
+++ b/libs/hwui/DeferredLayerUpdater.h
@@ -72,7 +72,7 @@
         mTransform = matrix ? new SkMatrix(*matrix) : 0;
     }
 
-    ANDROID_API void setDisplayList(DisplayList* displayList,
+    ANDROID_API void setDisplayList(RenderNode* displayList,
                 int left, int top, int right, int bottom);
 
     ANDROID_API void setPaint(const SkPaint* paint);
@@ -101,7 +101,7 @@
     // Layer type specific properties
     // displayList and surfaceTexture are mutually exclusive, only 1 may be set
     // dirtyRect is only valid if displayList is set
-    DisplayList* mDisplayList;
+    RenderNode* mDisplayList;
     Rect mDirtyRect;
     sp<GLConsumer> mSurfaceTexture;
     SkMatrix* mTransform;
diff --git a/libs/hwui/DisplayList.cpp b/libs/hwui/DisplayList.cpp
index 5c5a0425..346fbce 100644
--- a/libs/hwui/DisplayList.cpp
+++ b/libs/hwui/DisplayList.cpp
@@ -29,7 +29,7 @@
 namespace android {
 namespace uirenderer {
 
-void DisplayList::outputLogBuffer(int fd) {
+void RenderNode::outputLogBuffer(int fd) {
     DisplayListLogBuffer& logBuffer = DisplayListLogBuffer::getInstance();
     if (logBuffer.isEmpty()) {
         return;
@@ -48,65 +48,24 @@
     fflush(file);
 }
 
-DisplayList::DisplayList() :
-        mDisplayListData(0), mDestroyed(false), mTransformMatrix(NULL), mTransformCamera(NULL),
-        mTransformMatrix3D(NULL), mStaticMatrix(NULL), mAnimationMatrix(NULL) {
-
-    mLeft = 0;
-    mTop = 0;
-    mRight = 0;
-    mBottom = 0;
-    mClipToBounds = true;
-    mIsolatedZVolume = true;
-    mProjectBackwards = false;
-    mProjectionReceiver = false;
-    mOutline.rewind();
-    mClipToOutline = false;
-    mCastsShadow = false;
-    mUsesGlobalCamera = false;
-    mAlpha = 1;
-    mHasOverlappingRendering = true;
-    mTranslationX = 0;
-    mTranslationY = 0;
-    mTranslationZ = 0;
-    mRotation = 0;
-    mRotationX = 0;
-    mRotationY= 0;
-    mScaleX = 1;
-    mScaleY = 1;
-    mPivotX = 0;
-    mPivotY = 0;
-    mCameraDistance = 0;
-    mMatrixDirty = false;
-    mMatrixFlags = 0;
-    mPrevWidth = -1;
-    mPrevHeight = -1;
-    mWidth = 0;
-    mHeight = 0;
-    mPivotExplicitlySet = false;
-    mCaching = false;
+RenderNode::RenderNode() : mDestroyed(false), mDisplayListData(0) {
 }
 
-DisplayList::~DisplayList() {
+RenderNode::~RenderNode() {
     LOG_ALWAYS_FATAL_IF(mDestroyed, "Double destroyed DisplayList %p", this);
 
     mDestroyed = true;
     delete mDisplayListData;
-    delete mTransformMatrix;
-    delete mTransformCamera;
-    delete mTransformMatrix3D;
-    delete mStaticMatrix;
-    delete mAnimationMatrix;
 }
 
-void DisplayList::destroyDisplayListDeferred(DisplayList* displayList) {
+void RenderNode::destroyDisplayListDeferred(RenderNode* displayList) {
     if (displayList) {
         DISPLAY_LIST_LOGD("Deferring display list destruction");
         Caches::getInstance().deleteDisplayListDeferred(displayList);
     }
 }
 
-void DisplayList::setData(DisplayListData* data) {
+void RenderNode::setData(DisplayListData* data) {
     delete mDisplayListData;
     mDisplayListData = data;
     if (mDisplayListData) {
@@ -118,7 +77,7 @@
  * This function is a simplified version of replay(), where we simply retrieve and log the
  * display list. This function should remain in sync with the replay() function.
  */
-void DisplayList::output(uint32_t level) {
+void RenderNode::output(uint32_t level) {
     ALOGD("%*sStart display list (%p, %s, render=%d)", (level - 1) * 2, "", this,
             mName.string(), isRenderable());
     ALOGD("%*s%s %d", level * 2, "", "Save",
@@ -133,97 +92,35 @@
     ALOGD("%*sDone (%p, %s)", (level - 1) * 2, "", this, mName.string());
 }
 
-float DisplayList::getPivotX() {
-    updateMatrix();
-    return mPivotX;
-}
-
-float DisplayList::getPivotY() {
-    updateMatrix();
-    return mPivotY;
-}
-
-void DisplayList::updateMatrix() {
-    if (mMatrixDirty) {
-        // NOTE: mTransformMatrix won't be up to date if a DisplayList goes from a complex transform
-        // to a pure translate. This is safe because the matrix isn't read in pure translate cases.
-        if (mMatrixFlags && mMatrixFlags != TRANSLATION) {
-            if (!mTransformMatrix) {
-                // only allocate a matrix if we have a complex transform
-                mTransformMatrix = new Matrix4();
-            }
-            if (!mPivotExplicitlySet) {
-                if (mWidth != mPrevWidth || mHeight != mPrevHeight) {
-                    mPrevWidth = mWidth;
-                    mPrevHeight = mHeight;
-                    mPivotX = mPrevWidth / 2.0f;
-                    mPivotY = mPrevHeight / 2.0f;
-                }
-            }
-
-            if ((mMatrixFlags & ROTATION_3D) == 0) {
-                mTransformMatrix->loadTranslate(
-                        mPivotX + mTranslationX,
-                        mPivotY + mTranslationY,
-                        0);
-                mTransformMatrix->rotate(mRotation, 0, 0, 1);
-                mTransformMatrix->scale(mScaleX, mScaleY, 1);
-                mTransformMatrix->translate(-mPivotX, -mPivotY);
-            } else {
-                if (!mTransformCamera) {
-                    mTransformCamera = new Sk3DView();
-                    mTransformMatrix3D = new SkMatrix();
-                }
-                SkMatrix transformMatrix;
-                transformMatrix.reset();
-                mTransformCamera->save();
-                transformMatrix.preScale(mScaleX, mScaleY, mPivotX, mPivotY);
-                mTransformCamera->rotateX(mRotationX);
-                mTransformCamera->rotateY(mRotationY);
-                mTransformCamera->rotateZ(-mRotation);
-                mTransformCamera->getMatrix(mTransformMatrix3D);
-                mTransformMatrix3D->preTranslate(-mPivotX, -mPivotY);
-                mTransformMatrix3D->postTranslate(mPivotX + mTranslationX,
-                        mPivotY + mTranslationY);
-                transformMatrix.postConcat(*mTransformMatrix3D);
-                mTransformCamera->restore();
-
-                mTransformMatrix->load(transformMatrix);
-            }
-        }
-        mMatrixDirty = false;
+void RenderNode::outputViewProperties(const int level) {
+    properties().updateMatrix();
+    if (properties().mLeft != 0 || properties().mTop != 0) {
+        ALOGD("%*sTranslate (left, top) %d, %d", level * 2, "", properties().mLeft, properties().mTop);
     }
-}
-
-void DisplayList::outputViewProperties(const int level) {
-    updateMatrix();
-    if (mLeft != 0 || mTop != 0) {
-        ALOGD("%*sTranslate (left, top) %d, %d", level * 2, "", mLeft, mTop);
-    }
-    if (mStaticMatrix) {
+    if (properties().mStaticMatrix) {
         ALOGD("%*sConcatMatrix (static) %p: " SK_MATRIX_STRING,
-                level * 2, "", mStaticMatrix, SK_MATRIX_ARGS(mStaticMatrix));
+                level * 2, "", properties().mStaticMatrix, SK_MATRIX_ARGS(properties().mStaticMatrix));
     }
-    if (mAnimationMatrix) {
+    if (properties().mAnimationMatrix) {
         ALOGD("%*sConcatMatrix (animation) %p: " SK_MATRIX_STRING,
-                level * 2, "", mAnimationMatrix, SK_MATRIX_ARGS(mAnimationMatrix));
+                level * 2, "", properties().mAnimationMatrix, SK_MATRIX_ARGS(properties().mAnimationMatrix));
     }
-    if (mMatrixFlags != 0) {
-        if (mMatrixFlags == TRANSLATION) {
+    if (properties().mMatrixFlags != 0) {
+        if (properties().mMatrixFlags == TRANSLATION) {
             ALOGD("%*sTranslate %.2f, %.2f, %.2f",
-                    level * 2, "", mTranslationX, mTranslationY, mTranslationZ);
+                    level * 2, "", properties().mTranslationX, properties().mTranslationY, properties().mTranslationZ);
         } else {
             ALOGD("%*sConcatMatrix %p: " MATRIX_4_STRING,
-                    level * 2, "", mTransformMatrix, MATRIX_4_ARGS(mTransformMatrix));
+                    level * 2, "", properties().mTransformMatrix, MATRIX_4_ARGS(properties().mTransformMatrix));
         }
     }
 
-    bool clipToBoundsNeeded = mCaching ? false : mClipToBounds;
-    if (mAlpha < 1) {
-        if (mCaching) {
-            ALOGD("%*sSetOverrideLayerAlpha %.2f", level * 2, "", mAlpha);
-        } else if (!mHasOverlappingRendering) {
-            ALOGD("%*sScaleAlpha %.2f", level * 2, "", mAlpha);
+    bool clipToBoundsNeeded = properties().mCaching ? false : properties().mClipToBounds;
+    if (properties().mAlpha < 1) {
+        if (properties().mCaching) {
+            ALOGD("%*sSetOverrideLayerAlpha %.2f", level * 2, "", properties().mAlpha);
+        } else if (!properties().mHasOverlappingRendering) {
+            ALOGD("%*sScaleAlpha %.2f", level * 2, "", properties().mAlpha);
         } else {
             int flags = SkCanvas::kHasAlphaLayer_SaveFlag;
             if (clipToBoundsNeeded) {
@@ -231,51 +128,51 @@
                 clipToBoundsNeeded = false; // clipping done by save layer
             }
             ALOGD("%*sSaveLayerAlpha %.2f, %.2f, %.2f, %.2f, %d, 0x%x", level * 2, "",
-                    (float) 0, (float) 0, (float) mRight - mLeft, (float) mBottom - mTop,
-                    (int)(mAlpha * 255), flags);
+                    (float) 0, (float) 0, (float) properties().mRight - properties().mLeft, (float) properties().mBottom - properties().mTop,
+                    (int)(properties().mAlpha * 255), flags);
         }
     }
     if (clipToBoundsNeeded) {
         ALOGD("%*sClipRect %.2f, %.2f, %.2f, %.2f", level * 2, "", 0.0f, 0.0f,
-                (float) mRight - mLeft, (float) mBottom - mTop);
+                (float) properties().mRight - properties().mLeft, (float) properties().mBottom - properties().mTop);
     }
 }
 
 /*
  * For property operations, we pass a savecount of 0, since the operations aren't part of the
  * displaylist, and thus don't have to compensate for the record-time/playback-time discrepancy in
- * base saveCount (i.e., how RestoreToCount uses saveCount + mCount)
+ * base saveCount (i.e., how RestoreToCount uses saveCount + properties().mCount)
  */
 #define PROPERTY_SAVECOUNT 0
 
 template <class T>
-void DisplayList::setViewProperties(OpenGLRenderer& renderer, T& handler,
+void RenderNode::setViewProperties(OpenGLRenderer& renderer, T& handler,
         const int level) {
 #if DEBUG_DISPLAY_LIST
     outputViewProperties(level);
 #endif
-    updateMatrix();
-    if (mLeft != 0 || mTop != 0) {
-        renderer.translate(mLeft, mTop);
+    properties().updateMatrix();
+    if (properties().mLeft != 0 || properties().mTop != 0) {
+        renderer.translate(properties().mLeft, properties().mTop);
     }
-    if (mStaticMatrix) {
-        renderer.concatMatrix(mStaticMatrix);
-    } else if (mAnimationMatrix) {
-        renderer.concatMatrix(mAnimationMatrix);
+    if (properties().mStaticMatrix) {
+        renderer.concatMatrix(properties().mStaticMatrix);
+    } else if (properties().mAnimationMatrix) {
+        renderer.concatMatrix(properties().mAnimationMatrix);
     }
-    if (mMatrixFlags != 0) {
-        if (mMatrixFlags == TRANSLATION) {
-            renderer.translate(mTranslationX, mTranslationY);
+    if (properties().mMatrixFlags != 0) {
+        if (properties().mMatrixFlags == TRANSLATION) {
+            renderer.translate(properties().mTranslationX, properties().mTranslationY);
         } else {
-            renderer.concatMatrix(*mTransformMatrix);
+            renderer.concatMatrix(*properties().mTransformMatrix);
         }
     }
-    bool clipToBoundsNeeded = mCaching ? false : mClipToBounds;
-    if (mAlpha < 1) {
-        if (mCaching) {
-            renderer.setOverrideLayerAlpha(mAlpha);
-        } else if (!mHasOverlappingRendering) {
-            renderer.scaleAlpha(mAlpha);
+    bool clipToBoundsNeeded = properties().mCaching ? false : properties().mClipToBounds;
+    if (properties().mAlpha < 1) {
+        if (properties().mCaching) {
+            renderer.setOverrideLayerAlpha(properties().mAlpha);
+        } else if (!properties().mHasOverlappingRendering) {
+            renderer.scaleAlpha(properties().mAlpha);
         } else {
             // TODO: should be able to store the size of a DL at record time and not
             // have to pass it into this call. In fact, this information might be in the
@@ -287,56 +184,74 @@
             }
 
             SaveLayerOp* op = new (handler.allocator()) SaveLayerOp(
-                    0, 0, mRight - mLeft, mBottom - mTop, mAlpha * 255, saveFlags);
-            handler(op, PROPERTY_SAVECOUNT, mClipToBounds);
+                    0, 0, properties().mRight - properties().mLeft, properties().mBottom - properties().mTop, properties().mAlpha * 255, saveFlags);
+            handler(op, PROPERTY_SAVECOUNT, properties().mClipToBounds);
         }
     }
     if (clipToBoundsNeeded) {
         ClipRectOp* op = new (handler.allocator()) ClipRectOp(0, 0,
-                mRight - mLeft, mBottom - mTop, SkRegion::kIntersect_Op);
-        handler(op, PROPERTY_SAVECOUNT, mClipToBounds);
+                properties().mRight - properties().mLeft, properties().mBottom - properties().mTop, SkRegion::kIntersect_Op);
+        handler(op, PROPERTY_SAVECOUNT, properties().mClipToBounds);
     }
-    if (CC_UNLIKELY(mClipToOutline && !mOutline.isEmpty())) {
-        ClipPathOp* op = new (handler.allocator()) ClipPathOp(&mOutline, SkRegion::kIntersect_Op);
-        handler(op, PROPERTY_SAVECOUNT, mClipToBounds);
+    if (CC_UNLIKELY(properties().mClipToOutline && !properties().mOutline.isEmpty())) {
+        ClipPathOp* op = new (handler.allocator()) ClipPathOp(&properties().mOutline, SkRegion::kIntersect_Op);
+        handler(op, PROPERTY_SAVECOUNT, properties().mClipToBounds);
     }
 }
 
 /**
  * Apply property-based transformations to input matrix
+ *
+ * If true3dTransform is set to true, the transform applied to the input matrix will use true 4x4
+ * matrix computation instead of the Skia 3x3 matrix + camera hackery.
  */
-void DisplayList::applyViewPropertyTransforms(mat4& matrix) {
-    if (mLeft != 0 || mTop != 0) {
-        matrix.translate(mLeft, mTop);
+void RenderNode::applyViewPropertyTransforms(mat4& matrix, bool true3dTransform) {
+    if (properties().mLeft != 0 || properties().mTop != 0) {
+        matrix.translate(properties().mLeft, properties().mTop);
     }
-    if (mStaticMatrix) {
-        mat4 stat(*mStaticMatrix);
+    if (properties().mStaticMatrix) {
+        mat4 stat(*properties().mStaticMatrix);
         matrix.multiply(stat);
-    } else if (mAnimationMatrix) {
-        mat4 anim(*mAnimationMatrix);
+    } else if (properties().mAnimationMatrix) {
+        mat4 anim(*properties().mAnimationMatrix);
         matrix.multiply(anim);
     }
-    if (mMatrixFlags != 0) {
-        updateMatrix();
-        if (mMatrixFlags == TRANSLATION) {
-            matrix.translate(mTranslationX, mTranslationY, mTranslationZ);
+    if (properties().mMatrixFlags != 0) {
+        properties().updateMatrix();
+        if (properties().mMatrixFlags == TRANSLATION) {
+            matrix.translate(properties().mTranslationX, properties().mTranslationY,
+                    true3dTransform ? properties().mTranslationZ : 0.0f);
         } else {
-            matrix.multiply(*mTransformMatrix);
+            if (!true3dTransform) {
+                matrix.multiply(*properties().mTransformMatrix);
+            } else {
+                mat4 true3dMat;
+                true3dMat.loadTranslate(
+                        properties().mPivotX + properties().mTranslationX,
+                        properties().mPivotY + properties().mTranslationY,
+                        properties().mTranslationZ);
+                true3dMat.rotate(properties().mRotationX, 1, 0, 0);
+                true3dMat.rotate(properties().mRotationY, 0, 1, 0);
+                true3dMat.rotate(properties().mRotation, 0, 0, 1);
+                true3dMat.scale(properties().mScaleX, properties().mScaleY, 1);
+                true3dMat.translate(-properties().mPivotX, -properties().mPivotY);
+
+                matrix.multiply(true3dMat);
+            }
         }
     }
 }
 
 /**
- * Organizes the DisplayList hierarchy to prepare for Z-based draw order.
+ * Organizes the DisplayList hierarchy to prepare for background projection reordering.
  *
  * This should be called before a call to defer() or drawDisplayList()
  *
  * Each DisplayList that serves as a 3d root builds its list of composited children,
  * which are flagged to not draw in the standard draw loop.
  */
-void DisplayList::computeOrdering() {
+void RenderNode::computeOrdering() {
     ATRACE_CALL();
-    m3dNodes.clear();
     mProjectedNodes.clear();
 
     // TODO: create temporary DDLOp and call computeOrderingImpl on top DisplayList so that
@@ -345,40 +260,23 @@
     for (unsigned int i = 0; i < mDisplayListData->children.size(); i++) {
         DrawDisplayListOp* childOp = mDisplayListData->children[i];
         childOp->mDisplayList->computeOrderingImpl(childOp,
-                &m3dNodes, &mat4::identity(),
                 &mProjectedNodes, &mat4::identity());
     }
 }
 
-void DisplayList::computeOrderingImpl(
+void RenderNode::computeOrderingImpl(
         DrawDisplayListOp* opState,
-        Vector<ZDrawDisplayListOpPair>* compositedChildrenOf3dRoot,
-        const mat4* transformFrom3dRoot,
         Vector<DrawDisplayListOp*>* compositedChildrenOfProjectionSurface,
         const mat4* transformFromProjectionSurface) {
-    m3dNodes.clear();
     mProjectedNodes.clear();
     if (mDisplayListData == NULL || mDisplayListData->isEmpty()) return;
 
     // TODO: should avoid this calculation in most cases
     // TODO: just calculate single matrix, down to all leaf composited elements
-    Matrix4 localTransformFrom3dRoot(*transformFrom3dRoot);
-    localTransformFrom3dRoot.multiply(opState->mTransformFromParent);
     Matrix4 localTransformFromProjectionSurface(*transformFromProjectionSurface);
     localTransformFromProjectionSurface.multiply(opState->mTransformFromParent);
 
-    if (mTranslationZ != 0.0f) { // TODO: other signals for 3d compositing, such as custom matrix4
-        // composited 3d layer, flag for out of order draw and save matrix...
-        opState->mSkipInOrderDraw = true;
-        opState->mTransformFromCompositingAncestor.load(localTransformFrom3dRoot);
-
-        // ... and insert into current 3d root, keyed with pivot z for later sorting
-        Vector3 pivot(mPivotX, mPivotY, 0.0f);
-        mat4 totalTransform(localTransformFrom3dRoot);
-        applyViewPropertyTransforms(totalTransform);
-        totalTransform.mapPoint3d(pivot);
-        compositedChildrenOf3dRoot->add(ZDrawDisplayListOpPair(pivot.z, opState));
-    } else if (mProjectBackwards) {
+    if (properties().mProjectBackwards) {
         // composited projectee, flag for out of order draw, save matrix, and store in proj surface
         opState->mSkipInOrderDraw = true;
         opState->mTransformFromCompositingAncestor.load(localTransformFromProjectionSurface);
@@ -389,24 +287,15 @@
     }
 
     if (mDisplayListData->children.size() > 0) {
-        if (mIsolatedZVolume) {
-            // create a new 3d space for descendents by collecting them
-            compositedChildrenOf3dRoot = &m3dNodes;
-            transformFrom3dRoot = &mat4::identity();
-        } else {
-            applyViewPropertyTransforms(localTransformFrom3dRoot);
-            transformFrom3dRoot = &localTransformFrom3dRoot;
-        }
-
         const bool isProjectionReceiver = mDisplayListData->projectionReceiveIndex >= 0;
         bool haveAppliedPropertiesToProjection = false;
         for (unsigned int i = 0; i < mDisplayListData->children.size(); i++) {
             DrawDisplayListOp* childOp = mDisplayListData->children[i];
-            DisplayList* child = childOp->mDisplayList;
+            RenderNode* child = childOp->mDisplayList;
 
             Vector<DrawDisplayListOp*>* projectionChildren = NULL;
             const mat4* projectionTransform = NULL;
-            if (isProjectionReceiver && !child->mProjectBackwards) {
+            if (isProjectionReceiver && !child->properties().mProjectBackwards) {
                 // if receiving projections, collect projecting descendent
 
                 // Note that if a direct descendent is projecting backwards, we pass it's
@@ -422,9 +311,7 @@
                 projectionChildren = compositedChildrenOfProjectionSurface;
                 projectionTransform = &localTransformFromProjectionSurface;
             }
-            child->computeOrderingImpl(childOp,
-                    compositedChildrenOf3dRoot, transformFrom3dRoot,
-                    projectionChildren, projectionTransform);
+            child->computeOrderingImpl(childOp, projectionChildren, projectionTransform);
         }
     }
 
@@ -444,7 +331,7 @@
     const int mLevel;
 };
 
-void DisplayList::defer(DeferStateStruct& deferStruct, const int level) {
+void RenderNode::defer(DeferStateStruct& deferStruct, const int level) {
     DeferOperationHandler handler(deferStruct, level);
     iterate<DeferOperationHandler>(deferStruct.mRenderer, handler, level);
 }
@@ -455,7 +342,7 @@
         : mReplayStruct(replayStruct), mLevel(level) {}
     inline void operator()(DisplayListOp* operation, int saveCount, bool clipToBounds) {
 #if DEBUG_DISPLAY_LIST_OPS_AS_EVENTS
-        mReplayStruct.mRenderer.eventMark(operation->name());
+        properties().mReplayStruct.mRenderer.eventMark(operation->name());
 #endif
         operation->replay(mReplayStruct, saveCount, mLevel, clipToBounds);
     }
@@ -466,7 +353,7 @@
     const int mLevel;
 };
 
-void DisplayList::replay(ReplayStateStruct& replayStruct, const int level) {
+void RenderNode::replay(ReplayStateStruct& replayStruct, const int level) {
     ReplayOperationHandler handler(replayStruct, level);
 
     replayStruct.mRenderer.startMark(mName.string());
@@ -477,23 +364,45 @@
             replayStruct.mDrawGlStatus);
 }
 
+void RenderNode::buildZSortedChildList(Vector<ZDrawDisplayListOpPair>& zTranslatedNodes) {
+    if (mDisplayListData == NULL || mDisplayListData->children.size() == 0) return;
+
+    for (unsigned int i = 0; i < mDisplayListData->children.size(); i++) {
+        DrawDisplayListOp* childOp = mDisplayListData->children[i];
+        RenderNode* child = childOp->mDisplayList;
+        float childZ = child->properties().mTranslationZ;
+
+        if (childZ != 0.0f) {
+            zTranslatedNodes.add(ZDrawDisplayListOpPair(childZ, childOp));
+            childOp->mSkipInOrderDraw = true;
+        } else if (!child->properties().mProjectBackwards) {
+            // regular, in order drawing DisplayList
+            childOp->mSkipInOrderDraw = false;
+        }
+    }
+
+    // Z sort 3d children (stable-ness makes z compare fall back to standard drawing order)
+    std::stable_sort(zTranslatedNodes.begin(), zTranslatedNodes.end());
+}
+
 #define SHADOW_DELTA 0.1f
 
 template <class T>
-void DisplayList::iterate3dChildren(ChildrenSelectMode mode, OpenGLRenderer& renderer,
-        T& handler, const int level) {
-    if (m3dNodes.size() == 0 ||
-            (mode == kNegativeZChildren && m3dNodes[0].key > 0.0f) ||
-            (mode == kPositiveZChildren && m3dNodes[m3dNodes.size() - 1].key < 0.0f)) {
+void RenderNode::iterate3dChildren(const Vector<ZDrawDisplayListOpPair>& zTranslatedNodes,
+        ChildrenSelectMode mode, OpenGLRenderer& renderer, T& handler) {
+    const int size = zTranslatedNodes.size();
+    if (size == 0
+            || (mode == kNegativeZChildren && zTranslatedNodes[0].key > 0.0f)
+            || (mode == kPositiveZChildren && zTranslatedNodes[size - 1].key < 0.0f)) {
         // no 3d children to draw
         return;
     }
 
     int rootRestoreTo = renderer.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
     LinearAllocator& alloc = handler.allocator();
-    ClipRectOp* clipOp = new (alloc) ClipRectOp(0, 0, mWidth, mHeight,
+    ClipRectOp* clipOp = new (alloc) ClipRectOp(0, 0, properties().mWidth, properties().mHeight,
             SkRegion::kIntersect_Op); // clip to 3d root bounds
-    handler(clipOp, PROPERTY_SAVECOUNT, mClipToBounds);
+    handler(clipOp, PROPERTY_SAVECOUNT, properties().mClipToBounds);
 
     /**
      * Draw shadows and (potential) casters mostly in order, but allow the shadows of casters
@@ -502,7 +411,7 @@
      * This way, if Views A & B have the same Z height and are both casting shadows, the shadows are
      * underneath both, and neither's shadow is drawn on top of the other.
      */
-    const size_t nonNegativeIndex = findNonNegativeIndex(m3dNodes);
+    const size_t nonNegativeIndex = findNonNegativeIndex(zTranslatedNodes);
     size_t drawIndex, shadowIndex, endIndex;
     if (mode == kNegativeZChildren) {
         drawIndex = 0;
@@ -510,26 +419,32 @@
         shadowIndex = endIndex; // draw no shadows
     } else {
         drawIndex = nonNegativeIndex;
-        endIndex = m3dNodes.size();
+        endIndex = size;
         shadowIndex = drawIndex; // potentially draw shadow for each pos Z child
     }
     float lastCasterZ = 0.0f;
     while (shadowIndex < endIndex || drawIndex < endIndex) {
         if (shadowIndex < endIndex) {
-            DrawDisplayListOp* casterOp = m3dNodes[shadowIndex].value;
-            DisplayList* caster = casterOp->mDisplayList;
-            const float casterZ = m3dNodes[shadowIndex].key;
+            DrawDisplayListOp* casterOp = zTranslatedNodes[shadowIndex].value;
+            RenderNode* caster = casterOp->mDisplayList;
+            const float casterZ = zTranslatedNodes[shadowIndex].key;
             // attempt to render the shadow if the caster about to be drawn is its caster,
             // OR if its caster's Z value is similar to the previous potential caster
             if (shadowIndex == drawIndex || casterZ - lastCasterZ < SHADOW_DELTA) {
 
-                if (caster->mCastsShadow && caster->mAlpha > 0.0f) {
-                    mat4 shadowMatrix(casterOp->mTransformFromCompositingAncestor);
-                    caster->applyViewPropertyTransforms(shadowMatrix);
+                if (caster->properties().mCastsShadow && caster->properties().mAlpha > 0.0f) {
+                    mat4 shadowMatrixXY(casterOp->mTransformFromParent);
+                    caster->applyViewPropertyTransforms(shadowMatrixXY);
 
-                    DisplayListOp* shadowOp  = new (alloc) DrawShadowOp(shadowMatrix,
-                            caster->mAlpha, &(caster->mOutline), caster->mWidth, caster->mHeight);
-                    handler(shadowOp, PROPERTY_SAVECOUNT, mClipToBounds);
+                    // Z matrix needs actual 3d transformation, so mapped z values will be correct
+                    mat4 shadowMatrixZ(casterOp->mTransformFromParent);
+                    caster->applyViewPropertyTransforms(shadowMatrixZ, true);
+
+                    DisplayListOp* shadowOp  = new (alloc) DrawShadowOp(
+                            shadowMatrixXY, shadowMatrixZ,
+                            caster->properties().mAlpha, &(caster->properties().mOutline),
+                            caster->properties().mWidth, caster->properties().mHeight);
+                    handler(shadowOp, PROPERTY_SAVECOUNT, properties().mClipToBounds);
                 }
 
                 lastCasterZ = casterZ; // must do this even if current caster not casting a shadow
@@ -542,27 +457,27 @@
         // since it modifies the renderer's matrix
         int restoreTo = renderer.save(SkCanvas::kMatrix_SaveFlag);
 
-        DrawDisplayListOp* childOp = m3dNodes[drawIndex].value;
-        DisplayList* child = childOp->mDisplayList;
+        DrawDisplayListOp* childOp = zTranslatedNodes[drawIndex].value;
+        RenderNode* child = childOp->mDisplayList;
 
-        renderer.concatMatrix(childOp->mTransformFromCompositingAncestor);
+        renderer.concatMatrix(childOp->mTransformFromParent);
         childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone
-        handler(childOp, renderer.getSaveCount() - 1, mClipToBounds);
+        handler(childOp, renderer.getSaveCount() - 1, properties().mClipToBounds);
         childOp->mSkipInOrderDraw = true;
 
         renderer.restoreToCount(restoreTo);
         drawIndex++;
     }
-    handler(new (alloc) RestoreToCountOp(rootRestoreTo), PROPERTY_SAVECOUNT, mClipToBounds);
+    handler(new (alloc) RestoreToCountOp(rootRestoreTo), PROPERTY_SAVECOUNT, properties().mClipToBounds);
 }
 
 template <class T>
-void DisplayList::iterateProjectedChildren(OpenGLRenderer& renderer, T& handler, const int level) {
+void RenderNode::iterateProjectedChildren(OpenGLRenderer& renderer, T& handler, const int level) {
     int rootRestoreTo = renderer.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
     LinearAllocator& alloc = handler.allocator();
-    ClipRectOp* clipOp = new (alloc) ClipRectOp(0, 0, mWidth, mHeight,
+    ClipRectOp* clipOp = new (alloc) ClipRectOp(0, 0, properties().mWidth, properties().mHeight,
             SkRegion::kReplace_Op); // clip to projection surface root bounds
-    handler(clipOp, PROPERTY_SAVECOUNT, mClipToBounds);
+    handler(clipOp, PROPERTY_SAVECOUNT, properties().mClipToBounds);
 
     for (size_t i = 0; i < mProjectedNodes.size(); i++) {
         DrawDisplayListOp* childOp = mProjectedNodes[i];
@@ -571,11 +486,11 @@
         int restoreTo = renderer.save(SkCanvas::kMatrix_SaveFlag);
         renderer.concatMatrix(childOp->mTransformFromCompositingAncestor);
         childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone
-        handler(childOp, renderer.getSaveCount() - 1, mClipToBounds);
+        handler(childOp, renderer.getSaveCount() - 1, properties().mClipToBounds);
         childOp->mSkipInOrderDraw = true;
         renderer.restoreToCount(restoreTo);
     }
-    handler(new (alloc) RestoreToCountOp(rootRestoreTo), PROPERTY_SAVECOUNT, mClipToBounds);
+    handler(new (alloc) RestoreToCountOp(rootRestoreTo), PROPERTY_SAVECOUNT, properties().mClipToBounds);
 }
 
 /**
@@ -588,12 +503,12 @@
  * defer vs replay logic, per operation
  */
 template <class T>
-void DisplayList::iterate(OpenGLRenderer& renderer, T& handler, const int level) {
+void RenderNode::iterate(OpenGLRenderer& renderer, T& handler, const int level) {
     if (CC_UNLIKELY(mDestroyed)) { // temporary debug logging
-        ALOGW("Error: %s is drawing after destruction", getName());
+        ALOGW("Error: %s is drawing after destruction", mName.string());
         CRASH();
     }
-    if (mDisplayListData->isEmpty() || mAlpha <= 0) {
+    if (mDisplayListData->isEmpty() || properties().mAlpha <= 0) {
         DISPLAY_LIST_LOGD("%*sEmpty display list (%p, %s)", level * 2, "", this, mName.string());
         return;
     }
@@ -608,20 +523,20 @@
     LinearAllocator& alloc = handler.allocator();
     int restoreTo = renderer.getSaveCount();
     handler(new (alloc) SaveOp(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag),
-            PROPERTY_SAVECOUNT, mClipToBounds);
+            PROPERTY_SAVECOUNT, properties().mClipToBounds);
 
     DISPLAY_LIST_LOGD("%*sSave %d %d", (level + 1) * 2, "",
             SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag, restoreTo);
 
     setViewProperties<T>(renderer, handler, level + 1);
 
-    bool quickRejected = mClipToBounds && renderer.quickRejectConservative(0, 0, mWidth, mHeight);
+    bool quickRejected = properties().mClipToBounds && renderer.quickRejectConservative(0, 0, properties().mWidth, properties().mHeight);
     if (!quickRejected) {
-        // Z sort 3d children (stable-ness makes z compare fall back to standard drawing order)
-        std::stable_sort(m3dNodes.begin(), m3dNodes.end());
+        Vector<ZDrawDisplayListOpPair> zTranslatedNodes;
+        buildZSortedChildList(zTranslatedNodes);
 
         // for 3d root, draw children with negative z values
-        iterate3dChildren(kNegativeZChildren, renderer, handler, level);
+        iterate3dChildren(zTranslatedNodes, kNegativeZChildren, renderer, handler);
 
         DisplayListLogBuffer& logBuffer = DisplayListLogBuffer::getInstance();
         const int saveCountOffset = renderer.getSaveCount() - 1;
@@ -634,7 +549,7 @@
 #endif
 
             logBuffer.writeCommand(level, op->name());
-            handler(op, saveCountOffset, mClipToBounds);
+            handler(op, saveCountOffset, properties().mClipToBounds);
 
             if (CC_UNLIKELY(i == projectionReceiveIndex && mProjectedNodes.size() > 0)) {
                 iterateProjectedChildren(renderer, handler, level);
@@ -642,12 +557,12 @@
         }
 
         // for 3d root, draw children with positive z values
-        iterate3dChildren(kPositiveZChildren, renderer, handler, level);
+        iterate3dChildren(zTranslatedNodes, kPositiveZChildren, renderer, handler);
     }
 
     DISPLAY_LIST_LOGD("%*sRestoreToCount %d", (level + 1) * 2, "", restoreTo);
     handler(new (alloc) RestoreToCountOp(restoreTo),
-            PROPERTY_SAVECOUNT, mClipToBounds);
+            PROPERTY_SAVECOUNT, properties().mClipToBounds);
     renderer.setOverrideLayerAlpha(1.0f);
 }
 
diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h
index a3577d4..b80c118 100644
--- a/libs/hwui/DisplayList.h
+++ b/libs/hwui/DisplayList.h
@@ -40,12 +40,7 @@
 #include "Debug.h"
 #include "Matrix.h"
 #include "DeferredDisplayList.h"
-
-#define TRANSLATION 0x0001
-#define ROTATION    0x0002
-#define ROTATION_3D 0x0004
-#define SCALE       0x0008
-#define PIVOT       0x0010
+#include "RenderProperties.h"
 
 class SkBitmap;
 class SkPaint;
@@ -160,17 +155,17 @@
  * recorded stream of canvas operations is refreshed. The DisplayList (and its properties) stay
  * attached.
  */
-class DisplayList {
+class RenderNode {
 public:
-    ANDROID_API DisplayList();
-    ANDROID_API ~DisplayList();
+    ANDROID_API RenderNode();
+    ANDROID_API ~RenderNode();
 
     // See flags defined in DisplayList.java
     enum ReplayFlag {
         kReplayFlag_ClipChildren = 0x1
     };
 
-    ANDROID_API static void destroyDisplayListDeferred(DisplayList* displayList);
+    ANDROID_API static void destroyDisplayListDeferred(RenderNode* displayList);
     ANDROID_API static void outputLogBuffer(int fd);
 
     ANDROID_API void setData(DisplayListData* newData);
@@ -196,355 +191,20 @@
         }
     }
 
-    const char* getName() const {
-        return mName.string();
-    }
-
-    void setClipToBounds(bool clipToBounds) {
-        mClipToBounds = clipToBounds;
-    }
-
-    void setIsolatedZVolume(bool shouldIsolate) {
-        mIsolatedZVolume = shouldIsolate;
-    }
-
-    void setCastsShadow(bool castsShadow) {
-        mCastsShadow = castsShadow;
-    }
-
-    void setUsesGlobalCamera(bool usesGlobalCamera) {
-        mUsesGlobalCamera = usesGlobalCamera;
-    }
-
-    void setProjectBackwards(bool shouldProject) {
-        mProjectBackwards = shouldProject;
-    }
-
-    void setProjectionReceiver(bool shouldRecieve) {
-        mProjectionReceiver = shouldRecieve;
+    RenderProperties& properties() {
+        return mProperties;
     }
 
     bool isProjectionReceiver() {
-        return mProjectionReceiver;
-    }
-
-    void setOutline(const SkPath* outline) {
-        if (!outline) {
-            mOutline.reset();
-        } else {
-            mOutline = *outline;
-        }
-    }
-
-    void setClipToOutline(bool clipToOutline) {
-        mClipToOutline = clipToOutline;
-    }
-
-    void setStaticMatrix(SkMatrix* matrix) {
-        delete mStaticMatrix;
-        mStaticMatrix = new SkMatrix(*matrix);
-    }
-
-    // Can return NULL
-    SkMatrix* getStaticMatrix() {
-        return mStaticMatrix;
-    }
-
-    void setAnimationMatrix(SkMatrix* matrix) {
-        delete mAnimationMatrix;
-        if (matrix) {
-            mAnimationMatrix = new SkMatrix(*matrix);
-        } else {
-            mAnimationMatrix = NULL;
-        }
-    }
-
-    void setAlpha(float alpha) {
-        alpha = fminf(1.0f, fmaxf(0.0f, alpha));
-        if (alpha != mAlpha) {
-            mAlpha = alpha;
-        }
-    }
-
-    float getAlpha() const {
-        return mAlpha;
-    }
-
-    void setHasOverlappingRendering(bool hasOverlappingRendering) {
-        mHasOverlappingRendering = hasOverlappingRendering;
-    }
-
-    bool hasOverlappingRendering() const {
-        return mHasOverlappingRendering;
-    }
-
-    void setTranslationX(float translationX) {
-        if (translationX != mTranslationX) {
-            mTranslationX = translationX;
-            onTranslationUpdate();
-        }
-    }
-
-    float getTranslationX() const {
-        return mTranslationX;
-    }
-
-    void setTranslationY(float translationY) {
-        if (translationY != mTranslationY) {
-            mTranslationY = translationY;
-            onTranslationUpdate();
-        }
-    }
-
-    float getTranslationY() const {
-        return mTranslationY;
-    }
-
-    void setTranslationZ(float translationZ) {
-        if (translationZ != mTranslationZ) {
-            mTranslationZ = translationZ;
-            onTranslationUpdate();
-        }
-    }
-
-    float getTranslationZ() const {
-        return mTranslationZ;
-    }
-
-    void setRotation(float rotation) {
-        if (rotation != mRotation) {
-            mRotation = rotation;
-            mMatrixDirty = true;
-            if (mRotation == 0.0f) {
-                mMatrixFlags &= ~ROTATION;
-            } else {
-                mMatrixFlags |= ROTATION;
-            }
-        }
-    }
-
-    float getRotation() const {
-        return mRotation;
-    }
-
-    void setRotationX(float rotationX) {
-        if (rotationX != mRotationX) {
-            mRotationX = rotationX;
-            mMatrixDirty = true;
-            if (mRotationX == 0.0f && mRotationY == 0.0f) {
-                mMatrixFlags &= ~ROTATION_3D;
-            } else {
-                mMatrixFlags |= ROTATION_3D;
-            }
-        }
-    }
-
-    float getRotationX() const {
-        return mRotationX;
-    }
-
-    void setRotationY(float rotationY) {
-        if (rotationY != mRotationY) {
-            mRotationY = rotationY;
-            mMatrixDirty = true;
-            if (mRotationX == 0.0f && mRotationY == 0.0f) {
-                mMatrixFlags &= ~ROTATION_3D;
-            } else {
-                mMatrixFlags |= ROTATION_3D;
-            }
-        }
-    }
-
-    float getRotationY() const {
-        return mRotationY;
-    }
-
-    void setScaleX(float scaleX) {
-        if (scaleX != mScaleX) {
-            mScaleX = scaleX;
-            mMatrixDirty = true;
-            if (mScaleX == 1.0f && mScaleY == 1.0f) {
-                mMatrixFlags &= ~SCALE;
-            } else {
-                mMatrixFlags |= SCALE;
-            }
-        }
-    }
-
-    float getScaleX() const {
-        return mScaleX;
-    }
-
-    void setScaleY(float scaleY) {
-        if (scaleY != mScaleY) {
-            mScaleY = scaleY;
-            mMatrixDirty = true;
-            if (mScaleX == 1.0f && mScaleY == 1.0f) {
-                mMatrixFlags &= ~SCALE;
-            } else {
-                mMatrixFlags |= SCALE;
-            }
-        }
-    }
-
-    float getScaleY() const {
-        return mScaleY;
-    }
-
-    void setPivotX(float pivotX) {
-        mPivotX = pivotX;
-        mMatrixDirty = true;
-        if (mPivotX == 0.0f && mPivotY == 0.0f) {
-            mMatrixFlags &= ~PIVOT;
-        } else {
-            mMatrixFlags |= PIVOT;
-        }
-        mPivotExplicitlySet = true;
-    }
-
-    ANDROID_API float getPivotX();
-
-    void setPivotY(float pivotY) {
-        mPivotY = pivotY;
-        mMatrixDirty = true;
-        if (mPivotX == 0.0f && mPivotY == 0.0f) {
-            mMatrixFlags &= ~PIVOT;
-        } else {
-            mMatrixFlags |= PIVOT;
-        }
-        mPivotExplicitlySet = true;
-    }
-
-    ANDROID_API float getPivotY();
-
-    void setCameraDistance(float distance) {
-        if (distance != mCameraDistance) {
-            mCameraDistance = distance;
-            mMatrixDirty = true;
-            if (!mTransformCamera) {
-                mTransformCamera = new Sk3DView();
-                mTransformMatrix3D = new SkMatrix();
-            }
-            mTransformCamera->setCameraLocation(0, 0, distance);
-        }
-    }
-
-    float getCameraDistance() const {
-        return mCameraDistance;
-    }
-
-    void setLeft(int left) {
-        if (left != mLeft) {
-            mLeft = left;
-            mWidth = mRight - mLeft;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    float getLeft() const {
-        return mLeft;
-    }
-
-    void setTop(int top) {
-        if (top != mTop) {
-            mTop = top;
-            mHeight = mBottom - mTop;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    float getTop() const {
-        return mTop;
-    }
-
-    void setRight(int right) {
-        if (right != mRight) {
-            mRight = right;
-            mWidth = mRight - mLeft;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    float getRight() const {
-        return mRight;
-    }
-
-    void setBottom(int bottom) {
-        if (bottom != mBottom) {
-            mBottom = bottom;
-            mHeight = mBottom - mTop;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    float getBottom() const {
-        return mBottom;
-    }
-
-    void setLeftTop(int left, int top) {
-        if (left != mLeft || top != mTop) {
-            mLeft = left;
-            mTop = top;
-            mWidth = mRight - mLeft;
-            mHeight = mBottom - mTop;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    void setLeftTopRightBottom(int left, int top, int right, int bottom) {
-        if (left != mLeft || top != mTop || right != mRight || bottom != mBottom) {
-            mLeft = left;
-            mTop = top;
-            mRight = right;
-            mBottom = bottom;
-            mWidth = mRight - mLeft;
-            mHeight = mBottom - mTop;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    void offsetLeftRight(float offset) {
-        if (offset != 0) {
-            mLeft += offset;
-            mRight += offset;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    void offsetTopBottom(float offset) {
-        if (offset != 0) {
-            mTop += offset;
-            mBottom += offset;
-            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
-                mMatrixDirty = true;
-            }
-        }
-    }
-
-    void setCaching(bool caching) {
-        mCaching = caching;
+        return properties().isProjectionReceiver();
     }
 
     int getWidth() {
-        return mWidth;
+        return properties().getWidth();
     }
 
     int getHeight() {
-        return mHeight;
+        return properties().getHeight();
     }
 
 private:
@@ -562,31 +222,22 @@
         kPositiveZChildren
     };
 
-    void onTranslationUpdate() {
-        mMatrixDirty = true;
-        if (mTranslationX == 0.0f && mTranslationY == 0.0f && mTranslationZ == 0.0f) {
-            mMatrixFlags &= ~TRANSLATION;
-        } else {
-            mMatrixFlags |= TRANSLATION;
-        }
-    }
-
     void outputViewProperties(const int level);
 
-    void applyViewPropertyTransforms(mat4& matrix);
+    void applyViewPropertyTransforms(mat4& matrix, bool true3dTransform = false);
 
     void computeOrderingImpl(DrawDisplayListOp* opState,
-            Vector<ZDrawDisplayListOpPair>* compositedChildrenOf3dRoot,
-            const mat4* transformFrom3dRoot,
             Vector<DrawDisplayListOp*>* compositedChildrenOfProjectionSurface,
             const mat4* transformFromProjectionSurface);
 
     template <class T>
     inline void setViewProperties(OpenGLRenderer& renderer, T& handler, const int level);
 
+    void buildZSortedChildList(Vector<ZDrawDisplayListOpPair>& zTranslatedNodes);
+
     template <class T>
-    inline void iterate3dChildren(ChildrenSelectMode mode, OpenGLRenderer& renderer,
-        T& handler, const int level);
+    inline void iterate3dChildren(const Vector<ZDrawDisplayListOpPair>& zTranslatedNodes,
+            ChildrenSelectMode mode, OpenGLRenderer& renderer, T& handler);
 
     template <class T>
     inline void iterateProjectedChildren(OpenGLRenderer& renderer, T& handler, const int level);
@@ -594,8 +245,6 @@
     template <class T>
     inline void iterate(OpenGLRenderer& renderer, T& handler, const int level);
 
-    void updateMatrix();
-
     class TextContainer {
     public:
         size_t length() const {
@@ -610,56 +259,16 @@
         const char* mText;
     };
 
-    DisplayListData* mDisplayListData;
-
     String8 mName;
     bool mDestroyed; // used for debugging crash, TODO: remove once invalid state crash fixed
 
-    // Rendering properties
-    bool mClipToBounds;
-    bool mIsolatedZVolume;
-    bool mProjectBackwards;
-    bool mProjectionReceiver;
-    SkPath mOutline;
-    bool mClipToOutline;
-    bool mCastsShadow;
-    bool mUsesGlobalCamera; // TODO: respect value when rendering
-    float mAlpha;
-    bool mHasOverlappingRendering;
-    float mTranslationX, mTranslationY, mTranslationZ;
-    float mRotation, mRotationX, mRotationY;
-    float mScaleX, mScaleY;
-    float mPivotX, mPivotY;
-    float mCameraDistance;
-    int mLeft, mTop, mRight, mBottom;
-    int mWidth, mHeight;
-    int mPrevWidth, mPrevHeight;
-    bool mPivotExplicitlySet;
-    bool mMatrixDirty;
-    bool mMatrixIsIdentity;
-
-    /**
-     * Stores the total transformation of the DisplayList based upon its scalar
-     * translate/rotate/scale properties.
-     *
-     * In the common translation-only case, the matrix isn't allocated and the mTranslation
-     * properties are used directly.
-     */
-    Matrix4* mTransformMatrix;
-    uint32_t mMatrixFlags;
-    Sk3DView* mTransformCamera;
-    SkMatrix* mTransformMatrix3D;
-    SkMatrix* mStaticMatrix;
-    SkMatrix* mAnimationMatrix;
-    bool mCaching;
+    RenderProperties mProperties;
+    DisplayListData* mDisplayListData;
 
     /**
      * Draw time state - these properties are only set and used during rendering
      */
 
-    // for 3d roots, contains a z sorted list of all children items
-    Vector<ZDrawDisplayListOpPair> m3dNodes;
-
     // for projection surfaces, contains a list of all children items
     Vector<DrawDisplayListOp*> mProjectedNodes;
 }; // class DisplayList
diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h
index 65eda29..549b786 100644
--- a/libs/hwui/DisplayListOp.h
+++ b/libs/hwui/DisplayListOp.h
@@ -1488,9 +1488,9 @@
 };
 
 class DrawDisplayListOp : public DrawBoundedOp {
-    friend class DisplayList; // grant DisplayList access to info of child
+    friend class RenderNode; // grant DisplayList access to info of child
 public:
-    DrawDisplayListOp(DisplayList* displayList, int flags, const mat4& transformFromParent)
+    DrawDisplayListOp(RenderNode* displayList, int flags, const mat4& transformFromParent)
             : DrawBoundedOp(0, 0, displayList->getWidth(), displayList->getHeight(), 0),
             mDisplayList(displayList), mFlags(flags), mTransformFromParent(transformFromParent) {}
 
@@ -1522,7 +1522,7 @@
     virtual const char* name() { return "DrawDisplayList"; }
 
 private:
-    DisplayList* mDisplayList;
+    RenderNode* mDisplayList;
     const int mFlags;
 
     ///////////////////////////
@@ -1534,10 +1534,10 @@
     const mat4 mTransformFromParent;
 
     /**
-     * Holds the transformation between the 3d root OR projection surface ViewGroup and this
-     * DisplayList drawing instance. Represents any translations / transformations done within the
-     * drawing of the compositing ancestor ViewGroup's draw, before the draw of the View represented
-     * by this DisplayList draw instance.
+     * Holds the transformation between the projection surface ViewGroup and this DisplayList
+     * drawing instance. Represents any translations / transformations done within the drawing of
+     * the compositing ancestor ViewGroup's draw, before the draw of the View represented by this
+     * DisplayList draw instance.
      *
      * Note: doesn't include any transformation recorded within the DisplayList and its properties.
      */
@@ -1550,19 +1550,20 @@
  */
 class DrawShadowOp : public DrawOp {
 public:
-    DrawShadowOp(const mat4& transform, float alpha, const SkPath* outline,
+    DrawShadowOp(const mat4& transformXY, const mat4& transformZ, float alpha, const SkPath* outline,
             float fallbackWidth, float fallbackHeight)
-            : DrawOp(NULL), mTransform(transform), mAlpha(alpha), mOutline(outline),
+            : DrawOp(NULL), mTransformXY(transformXY), mTransformZ(transformZ),
+            mAlpha(alpha), mOutline(outline),
             mFallbackWidth(fallbackWidth), mFallbackHeight(fallbackHeight) {}
 
     virtual status_t applyDraw(OpenGLRenderer& renderer, Rect& dirty) {
-        if (!mOutline->isEmpty()) {
-            return renderer.drawShadow(mTransform, mAlpha, mOutline);
+        if (mOutline->isEmpty()) {
+            SkPath fakeOutline;
+            fakeOutline.addRect(0, 0, mFallbackWidth, mFallbackHeight);
+            return renderer.drawShadow(mTransformXY, mTransformZ, mAlpha, &fakeOutline);
         }
 
-        SkPath fakeOutline;
-        fakeOutline.addRect(0, 0, mFallbackWidth, mFallbackHeight);
-        return renderer.drawShadow(mTransform, mAlpha, &fakeOutline);
+        return renderer.drawShadow(mTransformXY, mTransformZ, mAlpha, mOutline);
     }
 
     virtual void output(int level, uint32_t logFlags) const {
@@ -1572,7 +1573,8 @@
     virtual const char* name() { return "DrawShadow"; }
 
 private:
-    const mat4 mTransform;
+    const mat4 mTransformXY;
+    const mat4 mTransformZ;
     const float mAlpha;
     const SkPath* mOutline;
     const float mFallbackWidth;
diff --git a/libs/hwui/DisplayListRenderer.cpp b/libs/hwui/DisplayListRenderer.cpp
index 3b1d567..e69e08e 100644
--- a/libs/hwui/DisplayListRenderer.cpp
+++ b/libs/hwui/DisplayListRenderer.cpp
@@ -179,7 +179,7 @@
     return StatefulBaseRenderer::clipRegion(region, op);
 }
 
-status_t DisplayListRenderer::drawDisplayList(DisplayList* displayList,
+status_t DisplayListRenderer::drawDisplayList(RenderNode* displayList,
         Rect& dirty, int32_t flags) {
     // dirty is an out parameter and should not be recorded,
     // it matters only when replaying the display list
diff --git a/libs/hwui/DisplayListRenderer.h b/libs/hwui/DisplayListRenderer.h
index 1fb72ce..65498a5 100644
--- a/libs/hwui/DisplayListRenderer.h
+++ b/libs/hwui/DisplayListRenderer.h
@@ -156,7 +156,7 @@
 // Canvas draw operations - special
 // ----------------------------------------------------------------------------
     virtual status_t drawLayer(Layer* layer, float x, float y);
-    virtual status_t drawDisplayList(DisplayList* displayList, Rect& dirty,
+    virtual status_t drawDisplayList(RenderNode* displayList, Rect& dirty,
             int32_t replayFlags);
 
     // TODO: rename for consistency
@@ -309,7 +309,7 @@
 
     int mRestoreSaveCount;
 
-    friend class DisplayList;
+    friend class RenderNode;
 
 }; // class DisplayListRenderer
 
diff --git a/libs/hwui/Layer.cpp b/libs/hwui/Layer.cpp
index 8992a13..52176d4 100644
--- a/libs/hwui/Layer.cpp
+++ b/libs/hwui/Layer.cpp
@@ -194,7 +194,7 @@
     deferredList = new DeferredDisplayList(dirtyRect);
 
     DeferStateStruct deferredState(*deferredList, *renderer,
-            DisplayList::kReplayFlag_ClipChildren);
+            RenderNode::kReplayFlag_ClipChildren);
 
     renderer->initViewport(width, height);
     renderer->setupFrameState(dirtyRect.left, dirtyRect.top,
@@ -238,7 +238,7 @@
     renderer->prepareDirty(dirtyRect.left, dirtyRect.top, dirtyRect.right, dirtyRect.bottom,
             !isBlend());
 
-    renderer->drawDisplayList(displayList, dirtyRect, DisplayList::kReplayFlag_ClipChildren);
+    renderer->drawDisplayList(displayList, dirtyRect, RenderNode::kReplayFlag_ClipChildren);
 
     renderer->finish();
     renderer = NULL;
diff --git a/libs/hwui/Layer.h b/libs/hwui/Layer.h
index f6538f2..d8440ea 100644
--- a/libs/hwui/Layer.h
+++ b/libs/hwui/Layer.h
@@ -43,7 +43,7 @@
 // Forward declarations
 class Caches;
 class OpenGLRenderer;
-class DisplayList;
+class RenderNode;
 class DeferredDisplayList;
 class DeferStateStruct;
 
@@ -84,7 +84,7 @@
         regionRect.translate(layer.left, layer.top);
     }
 
-    void updateDeferred(OpenGLRenderer* renderer, DisplayList* displayList,
+    void updateDeferred(OpenGLRenderer* renderer, RenderNode* displayList,
             int left, int top, int right, int bottom) {
         this->renderer = renderer;
         this->displayList = displayList;
@@ -294,7 +294,7 @@
      */
     bool deferredUpdateScheduled;
     OpenGLRenderer* renderer;
-    DisplayList* displayList;
+    RenderNode* displayList;
     Rect dirtyRect;
     bool debugDrawUpdate;
     bool hasDrawnSinceUpdate;
diff --git a/libs/hwui/Matrix.cpp b/libs/hwui/Matrix.cpp
index 3d6188a..f06106b 100644
--- a/libs/hwui/Matrix.cpp
+++ b/libs/hwui/Matrix.cpp
@@ -385,9 +385,14 @@
     mType = kTypeTranslate | kTypeScale | kTypeRectToRect;
 }
 
+float Matrix4::mapZ(const Vector3& orig) const {
+    // duplicates logic for mapPoint3d's z coordinate
+    return orig.x * data[2] + orig.y * data[6] + orig.z * data[kScaleZ] + data[kTranslateZ];
+}
+
 void Matrix4::mapPoint3d(Vector3& vec) const {
     //TODO: optimize simple case
-    Vector3 orig(vec);
+    const Vector3 orig(vec);
     vec.x = orig.x * data[kScaleX] + orig.y * data[kSkewX] + orig.z * data[8] + data[kTranslateX];
     vec.y = orig.x * data[kSkewY] + orig.y * data[kScaleY] + orig.z * data[9] + data[kTranslateY];
     vec.z = orig.x * data[2] + orig.y * data[6] + orig.z * data[kScaleZ] + data[kTranslateZ];
diff --git a/libs/hwui/Matrix.h b/libs/hwui/Matrix.h
index 8b586f0..26cb05f 100644
--- a/libs/hwui/Matrix.h
+++ b/libs/hwui/Matrix.h
@@ -199,6 +199,7 @@
     void copyTo(float* v) const;
     void copyTo(SkMatrix& v) const;
 
+    float mapZ(const Vector3& orig) const;
     void mapPoint3d(Vector3& vec) const;
     void mapPoint(float& x, float& y) const; // 2d only
     void mapRect(Rect& r) const; // 2d only
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index 7002e26..1475953 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -1912,7 +1912,7 @@
 // Drawing
 ///////////////////////////////////////////////////////////////////////////////
 
-status_t OpenGLRenderer::drawDisplayList(DisplayList* displayList, Rect& dirty,
+status_t OpenGLRenderer::drawDisplayList(RenderNode* displayList, Rect& dirty,
         int32_t replayFlags) {
     status_t status;
     // All the usual checks and setup operations (quickReject, setupDraw, etc.)
@@ -3190,8 +3190,16 @@
     return drawColorRects(rects, count, paint, false, true, true);
 }
 
-status_t OpenGLRenderer::drawShadow(const mat4& casterTransform, float casterAlpha,
-        const SkPath* casterOutline) {
+static void mapPointFakeZ(Vector3& point, const mat4& transformXY, const mat4& transformZ) {
+    // map z coordinate with true 3d matrix
+    point.z = transformZ.mapZ(point);
+
+    // map x,y coordinates with draw/Skia matrix
+    transformXY.mapPoint(point.x, point.y);
+}
+
+status_t OpenGLRenderer::drawShadow(const mat4& casterTransformXY, const mat4& casterTransformZ,
+        float casterAlpha, const SkPath* casterOutline) {
     if (currentSnapshot()->isIgnored()) return DrawGlInfo::kStatusDone;
 
     // TODO: use quickRejectWithScissor. For now, always force enable scissor.
@@ -3214,10 +3222,12 @@
     // map 2d caster poly into 3d
     const int casterVertexCount = casterVertices2d.size();
     Vector3 casterPolygon[casterVertexCount];
+    float minZ = FLT_MAX;
     for (int i = 0; i < casterVertexCount; i++) {
         const Vertex& point2d = casterVertices2d[i];
         casterPolygon[i] = Vector3(point2d.x, point2d.y, 0);
-        casterTransform.mapPoint3d(casterPolygon[i]);
+        mapPointFakeZ(casterPolygon[i], casterTransformXY, casterTransformZ);
+        minZ = fmin(minZ, casterPolygon[i].z);
     }
 
     // map the centroid of the caster into 3d
@@ -3225,7 +3235,16 @@
             reinterpret_cast<const Vector2*>(casterVertices2d.array()),
             casterVertexCount);
     Vector3 centroid3d(centroid.x, centroid.y, 0);
-    casterTransform.mapPoint3d(centroid3d);
+    mapPointFakeZ(centroid3d, casterTransformXY, casterTransformZ);
+
+    // if the caster intersects the z=0 plane, lift it in Z so it doesn't
+    if (minZ < SHADOW_MIN_CASTER_Z) {
+        float casterLift = SHADOW_MIN_CASTER_Z - minZ;
+        for (int i = 0; i < casterVertexCount; i++) {
+            casterPolygon[i].z += casterLift;
+        }
+        centroid3d.z += casterLift;
+    }
 
     // draw caster's shadows
     if (mCaches.propertyAmbientShadowStrength > 0) {
@@ -3244,7 +3263,6 @@
         ShadowTessellator::tessellateSpotShadow(casterPolygon, casterVertexCount,
                 lightPosScale, *currentTransform(), getWidth(), getHeight(),
                 spotShadowVertexBuffer);
-
         drawVertexBuffer(kVertexBufferMode_Shadow, spotShadowVertexBuffer, &paint);
     }
 
diff --git a/libs/hwui/OpenGLRenderer.h b/libs/hwui/OpenGLRenderer.h
index 03beae3..94abfa7 100644
--- a/libs/hwui/OpenGLRenderer.h
+++ b/libs/hwui/OpenGLRenderer.h
@@ -54,7 +54,7 @@
 namespace uirenderer {
 
 class DeferredDisplayState;
-class DisplayList;
+class RenderNode;
 class TextSetupFunctor;
 class VertexBuffer;
 class SkiaShader;
@@ -165,7 +165,7 @@
     int saveLayerDeferred(float left, float top, float right, float bottom,
             const SkPaint* paint, int flags);
 
-    virtual status_t drawDisplayList(DisplayList* displayList, Rect& dirty, int32_t replayFlags = 1);
+    virtual status_t drawDisplayList(RenderNode* displayList, Rect& dirty, int32_t replayFlags = 1);
     virtual status_t drawLayer(Layer* layer, float x, float y);
     virtual status_t drawBitmap(const SkBitmap* bitmap, float left, float top,
             const SkPaint* paint);
@@ -208,8 +208,8 @@
             DrawOpMode drawOpMode = kDrawOpMode_Immediate);
     virtual status_t drawRects(const float* rects, int count, const SkPaint* paint);
 
-    status_t drawShadow(const mat4& casterTransform, float casterAlpha,
-            const SkPath* casterOutline);
+    status_t drawShadow(const mat4& casterTransformXY, const mat4& casterTransformZ,
+            float casterAlpha, const SkPath* casterOutline);
 
     virtual void resetShader();
     virtual void setupShader(SkiaShader* shader);
diff --git a/libs/hwui/RenderProperties.cpp b/libs/hwui/RenderProperties.cpp
new file mode 100644
index 0000000..714fd1b
--- /dev/null
+++ b/libs/hwui/RenderProperties.cpp
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+#include "RenderProperties.h"
+
+#include <SkMatrix.h>
+
+#include "Matrix.h"
+
+namespace android {
+namespace uirenderer {
+
+RenderProperties::RenderProperties()
+        : mClipToBounds(true)
+        , mProjectBackwards(false)
+        , mProjectionReceiver(false)
+        , mClipToOutline(false)
+        , mCastsShadow(false)
+        , mUsesGlobalCamera(false) // TODO: respect value when rendering
+        , mAlpha(1)
+        , mHasOverlappingRendering(true)
+        , mTranslationX(0), mTranslationY(0), mTranslationZ(0)
+        , mRotation(0), mRotationX(0), mRotationY(0)
+        , mScaleX(1), mScaleY(1)
+        , mPivotX(0), mPivotY(0)
+        , mCameraDistance(0)
+        , mLeft(0), mTop(0), mRight(0), mBottom(0)
+        , mWidth(0), mHeight(0)
+        , mPrevWidth(-1), mPrevHeight(-1)
+        , mPivotExplicitlySet(false)
+        , mMatrixDirty(false)
+        , mMatrixIsIdentity(true)
+        , mTransformMatrix(NULL)
+        , mMatrixFlags(0)
+        , mTransformCamera(NULL)
+        , mTransformMatrix3D(NULL)
+        , mStaticMatrix(NULL)
+        , mAnimationMatrix(NULL)
+        , mCaching(false) {
+    mOutline.rewind();
+}
+
+RenderProperties::~RenderProperties() {
+    delete mTransformMatrix;
+    delete mTransformCamera;
+    delete mTransformMatrix3D;
+    delete mStaticMatrix;
+    delete mAnimationMatrix;
+}
+
+float RenderProperties::getPivotX() {
+    updateMatrix();
+    return mPivotX;
+}
+
+float RenderProperties::getPivotY() {
+    updateMatrix();
+    return mPivotY;
+}
+
+void RenderProperties::updateMatrix() {
+    if (mMatrixDirty) {
+        // NOTE: mTransformMatrix won't be up to date if a DisplayList goes from a complex transform
+        // to a pure translate. This is safe because the matrix isn't read in pure translate cases.
+        if (mMatrixFlags && mMatrixFlags != TRANSLATION) {
+            if (!mTransformMatrix) {
+                // only allocate a matrix if we have a complex transform
+                mTransformMatrix = new Matrix4();
+            }
+            if (!mPivotExplicitlySet) {
+                if (mWidth != mPrevWidth || mHeight != mPrevHeight) {
+                    mPrevWidth = mWidth;
+                    mPrevHeight = mHeight;
+                    mPivotX = mPrevWidth / 2.0f;
+                    mPivotY = mPrevHeight / 2.0f;
+                }
+            }
+
+            if ((mMatrixFlags & ROTATION_3D) == 0) {
+                mTransformMatrix->loadTranslate(
+                        mPivotX + mTranslationX,
+                        mPivotY + mTranslationY,
+                        0);
+                mTransformMatrix->rotate(mRotation, 0, 0, 1);
+                mTransformMatrix->scale(mScaleX, mScaleY, 1);
+                mTransformMatrix->translate(-mPivotX, -mPivotY);
+            } else {
+                if (!mTransformCamera) {
+                    mTransformCamera = new Sk3DView();
+                    mTransformMatrix3D = new SkMatrix();
+                }
+                SkMatrix transformMatrix;
+                transformMatrix.reset();
+                mTransformCamera->save();
+                transformMatrix.preScale(mScaleX, mScaleY, mPivotX, mPivotY);
+                mTransformCamera->rotateX(mRotationX);
+                mTransformCamera->rotateY(mRotationY);
+                mTransformCamera->rotateZ(-mRotation);
+                mTransformCamera->getMatrix(mTransformMatrix3D);
+                mTransformMatrix3D->preTranslate(-mPivotX, -mPivotY);
+                mTransformMatrix3D->postTranslate(mPivotX + mTranslationX,
+                        mPivotY + mTranslationY);
+                transformMatrix.postConcat(*mTransformMatrix3D);
+                mTransformCamera->restore();
+
+                mTransformMatrix->load(transformMatrix);
+            }
+        }
+        mMatrixDirty = false;
+    }
+}
+
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h
new file mode 100644
index 0000000..a5ce4a6
--- /dev/null
+++ b/libs/hwui/RenderProperties.h
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+#ifndef RENDERNODEPROPERTIES_H_
+#define RENDERNODEPROPERTIES_H_
+
+#include <stddef.h>
+#include <cutils/compiler.h>
+#include <androidfw/ResourceTypes.h>
+
+#include <SkCamera.h>
+#include <SkMatrix.h>
+#include <SkPath.h>
+
+#define TRANSLATION 0x0001
+#define ROTATION    0x0002
+#define ROTATION_3D 0x0004
+#define SCALE       0x0008
+#define PIVOT       0x0010
+
+class SkBitmap;
+class SkPaint;
+class SkRegion;
+
+namespace android {
+namespace uirenderer {
+
+class Matrix4;
+class RenderNode;
+
+/*
+ * Data structure that holds the properties for a RenderNode
+ */
+class RenderProperties {
+public:
+    RenderProperties();
+    virtual ~RenderProperties();
+
+    void setClipToBounds(bool clipToBounds) {
+        mClipToBounds = clipToBounds;
+    }
+
+    void setCastsShadow(bool castsShadow) {
+        mCastsShadow = castsShadow;
+    }
+
+    void setUsesGlobalCamera(bool usesGlobalCamera) {
+        mUsesGlobalCamera = usesGlobalCamera;
+    }
+
+    void setProjectBackwards(bool shouldProject) {
+        mProjectBackwards = shouldProject;
+    }
+
+    void setProjectionReceiver(bool shouldRecieve) {
+        mProjectionReceiver = shouldRecieve;
+    }
+
+    bool isProjectionReceiver() {
+        return mProjectionReceiver;
+    }
+
+    void setOutline(const SkPath* outline) {
+        if (!outline) {
+            mOutline.reset();
+        } else {
+            mOutline = *outline;
+        }
+    }
+
+    void setClipToOutline(bool clipToOutline) {
+        mClipToOutline = clipToOutline;
+    }
+
+    void setStaticMatrix(SkMatrix* matrix) {
+        delete mStaticMatrix;
+        mStaticMatrix = new SkMatrix(*matrix);
+    }
+
+    // Can return NULL
+    SkMatrix* getStaticMatrix() {
+        return mStaticMatrix;
+    }
+
+    void setAnimationMatrix(SkMatrix* matrix) {
+        delete mAnimationMatrix;
+        if (matrix) {
+            mAnimationMatrix = new SkMatrix(*matrix);
+        } else {
+            mAnimationMatrix = NULL;
+        }
+    }
+
+    void setAlpha(float alpha) {
+        alpha = fminf(1.0f, fmaxf(0.0f, alpha));
+        if (alpha != mAlpha) {
+            mAlpha = alpha;
+        }
+    }
+
+    float getAlpha() const {
+        return mAlpha;
+    }
+
+    void setHasOverlappingRendering(bool hasOverlappingRendering) {
+        mHasOverlappingRendering = hasOverlappingRendering;
+    }
+
+    bool hasOverlappingRendering() const {
+        return mHasOverlappingRendering;
+    }
+
+    void setTranslationX(float translationX) {
+        if (translationX != mTranslationX) {
+            mTranslationX = translationX;
+            onTranslationUpdate();
+        }
+    }
+
+    float getTranslationX() const {
+        return mTranslationX;
+    }
+
+    void setTranslationY(float translationY) {
+        if (translationY != mTranslationY) {
+            mTranslationY = translationY;
+            onTranslationUpdate();
+        }
+    }
+
+    float getTranslationY() const {
+        return mTranslationY;
+    }
+
+    void setTranslationZ(float translationZ) {
+        if (translationZ != mTranslationZ) {
+            mTranslationZ = translationZ;
+            onTranslationUpdate();
+        }
+    }
+
+    float getTranslationZ() const {
+        return mTranslationZ;
+    }
+
+    void setRotation(float rotation) {
+        if (rotation != mRotation) {
+            mRotation = rotation;
+            mMatrixDirty = true;
+            if (mRotation == 0.0f) {
+                mMatrixFlags &= ~ROTATION;
+            } else {
+                mMatrixFlags |= ROTATION;
+            }
+        }
+    }
+
+    float getRotation() const {
+        return mRotation;
+    }
+
+    void setRotationX(float rotationX) {
+        if (rotationX != mRotationX) {
+            mRotationX = rotationX;
+            mMatrixDirty = true;
+            if (mRotationX == 0.0f && mRotationY == 0.0f) {
+                mMatrixFlags &= ~ROTATION_3D;
+            } else {
+                mMatrixFlags |= ROTATION_3D;
+            }
+        }
+    }
+
+    float getRotationX() const {
+        return mRotationX;
+    }
+
+    void setRotationY(float rotationY) {
+        if (rotationY != mRotationY) {
+            mRotationY = rotationY;
+            mMatrixDirty = true;
+            if (mRotationX == 0.0f && mRotationY == 0.0f) {
+                mMatrixFlags &= ~ROTATION_3D;
+            } else {
+                mMatrixFlags |= ROTATION_3D;
+            }
+        }
+    }
+
+    float getRotationY() const {
+        return mRotationY;
+    }
+
+    void setScaleX(float scaleX) {
+        if (scaleX != mScaleX) {
+            mScaleX = scaleX;
+            mMatrixDirty = true;
+            if (mScaleX == 1.0f && mScaleY == 1.0f) {
+                mMatrixFlags &= ~SCALE;
+            } else {
+                mMatrixFlags |= SCALE;
+            }
+        }
+    }
+
+    float getScaleX() const {
+        return mScaleX;
+    }
+
+    void setScaleY(float scaleY) {
+        if (scaleY != mScaleY) {
+            mScaleY = scaleY;
+            mMatrixDirty = true;
+            if (mScaleX == 1.0f && mScaleY == 1.0f) {
+                mMatrixFlags &= ~SCALE;
+            } else {
+                mMatrixFlags |= SCALE;
+            }
+        }
+    }
+
+    float getScaleY() const {
+        return mScaleY;
+    }
+
+    void setPivotX(float pivotX) {
+        mPivotX = pivotX;
+        mMatrixDirty = true;
+        if (mPivotX == 0.0f && mPivotY == 0.0f) {
+            mMatrixFlags &= ~PIVOT;
+        } else {
+            mMatrixFlags |= PIVOT;
+        }
+        mPivotExplicitlySet = true;
+    }
+
+    ANDROID_API float getPivotX();
+
+    void setPivotY(float pivotY) {
+        mPivotY = pivotY;
+        mMatrixDirty = true;
+        if (mPivotX == 0.0f && mPivotY == 0.0f) {
+            mMatrixFlags &= ~PIVOT;
+        } else {
+            mMatrixFlags |= PIVOT;
+        }
+        mPivotExplicitlySet = true;
+    }
+
+    ANDROID_API float getPivotY();
+
+    void setCameraDistance(float distance) {
+        if (distance != mCameraDistance) {
+            mCameraDistance = distance;
+            mMatrixDirty = true;
+            if (!mTransformCamera) {
+                mTransformCamera = new Sk3DView();
+                mTransformMatrix3D = new SkMatrix();
+            }
+            mTransformCamera->setCameraLocation(0, 0, distance);
+        }
+    }
+
+    float getCameraDistance() const {
+        return mCameraDistance;
+    }
+
+    void setLeft(int left) {
+        if (left != mLeft) {
+            mLeft = left;
+            mWidth = mRight - mLeft;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    float getLeft() const {
+        return mLeft;
+    }
+
+    void setTop(int top) {
+        if (top != mTop) {
+            mTop = top;
+            mHeight = mBottom - mTop;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    float getTop() const {
+        return mTop;
+    }
+
+    void setRight(int right) {
+        if (right != mRight) {
+            mRight = right;
+            mWidth = mRight - mLeft;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    float getRight() const {
+        return mRight;
+    }
+
+    void setBottom(int bottom) {
+        if (bottom != mBottom) {
+            mBottom = bottom;
+            mHeight = mBottom - mTop;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    float getBottom() const {
+        return mBottom;
+    }
+
+    void setLeftTop(int left, int top) {
+        if (left != mLeft || top != mTop) {
+            mLeft = left;
+            mTop = top;
+            mWidth = mRight - mLeft;
+            mHeight = mBottom - mTop;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    void setLeftTopRightBottom(int left, int top, int right, int bottom) {
+        if (left != mLeft || top != mTop || right != mRight || bottom != mBottom) {
+            mLeft = left;
+            mTop = top;
+            mRight = right;
+            mBottom = bottom;
+            mWidth = mRight - mLeft;
+            mHeight = mBottom - mTop;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    void offsetLeftRight(float offset) {
+        if (offset != 0) {
+            mLeft += offset;
+            mRight += offset;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    void offsetTopBottom(float offset) {
+        if (offset != 0) {
+            mTop += offset;
+            mBottom += offset;
+            if (mMatrixFlags > TRANSLATION && !mPivotExplicitlySet) {
+                mMatrixDirty = true;
+            }
+        }
+    }
+
+    void setCaching(bool caching) {
+        mCaching = caching;
+    }
+
+    int getWidth() {
+        return mWidth;
+    }
+
+    int getHeight() {
+        return mHeight;
+    }
+
+private:
+    void onTranslationUpdate() {
+        mMatrixDirty = true;
+        if (mTranslationX == 0.0f && mTranslationY == 0.0f && mTranslationZ == 0.0f) {
+            mMatrixFlags &= ~TRANSLATION;
+        } else {
+            mMatrixFlags |= TRANSLATION;
+        }
+    }
+
+    void updateMatrix();
+
+    // Rendering properties
+    bool mClipToBounds;
+    bool mProjectBackwards;
+    bool mProjectionReceiver;
+    SkPath mOutline;
+    bool mClipToOutline;
+    bool mCastsShadow;
+    bool mUsesGlobalCamera; // TODO: respect value when rendering
+    float mAlpha;
+    bool mHasOverlappingRendering;
+    float mTranslationX, mTranslationY, mTranslationZ;
+    float mRotation, mRotationX, mRotationY;
+    float mScaleX, mScaleY;
+    float mPivotX, mPivotY;
+    float mCameraDistance;
+    int mLeft, mTop, mRight, mBottom;
+    int mWidth, mHeight;
+    int mPrevWidth, mPrevHeight;
+    bool mPivotExplicitlySet;
+    bool mMatrixDirty;
+    bool mMatrixIsIdentity;
+
+    /**
+     * Stores the total transformation of the DisplayList based upon its scalar
+     * translate/rotate/scale properties.
+     *
+     * In the common translation-only case, the matrix isn't allocated and the mTranslation
+     * properties are used directly.
+     */
+    Matrix4* mTransformMatrix;
+    uint32_t mMatrixFlags;
+    Sk3DView* mTransformCamera;
+    SkMatrix* mTransformMatrix3D;
+    SkMatrix* mStaticMatrix;
+    SkMatrix* mAnimationMatrix;
+    bool mCaching;
+
+    friend class RenderNode;
+};
+
+} /* namespace uirenderer */
+} /* namespace android */
+
+#endif /* RENDERNODEPROPERTIES_H_ */
diff --git a/libs/hwui/Renderer.h b/libs/hwui/Renderer.h
index 4754bad..efcea5f 100644
--- a/libs/hwui/Renderer.h
+++ b/libs/hwui/Renderer.h
@@ -31,7 +31,7 @@
 
 namespace uirenderer {
 
-class DisplayList;
+class RenderNode;
 class Layer;
 class Matrix4;
 class SkiaColorFilter;
@@ -232,7 +232,7 @@
 // Canvas draw operations - special
 // ----------------------------------------------------------------------------
     virtual status_t drawLayer(Layer* layer, float x, float y) = 0;
-    virtual status_t drawDisplayList(DisplayList* displayList, Rect& dirty,
+    virtual status_t drawDisplayList(RenderNode* displayList, Rect& dirty,
             int32_t replayFlags) = 0;
 
     // TODO: rename for consistency
diff --git a/libs/hwui/ShadowTessellator.h b/libs/hwui/ShadowTessellator.h
index 120774b..c558460 100644
--- a/libs/hwui/ShadowTessellator.h
+++ b/libs/hwui/ShadowTessellator.h
@@ -54,6 +54,8 @@
 // The total number of indices used for drawing the shadow geometry as triangle strips.
 #define SHADOW_INDEX_COUNT (2 * SHADOW_RAY_COUNT + 1 + 2 * (SHADOW_RAY_COUNT + 1))
 
+#define SHADOW_MIN_CASTER_Z 0.001f
+
 class ShadowTessellator {
 public:
     static void tessellateAmbientShadow(const Vector3* casterPolygon,
diff --git a/libs/hwui/SpotShadow.cpp b/libs/hwui/SpotShadow.cpp
index 54039c01..8538b29 100644
--- a/libs/hwui/SpotShadow.cpp
+++ b/libs/hwui/SpotShadow.cpp
@@ -35,7 +35,7 @@
  * Calculate the angle between and x and a y coordinate.
  * The atan2 range from -PI to PI.
  */
-float angle(const Vector2& point, const Vector2& center) {
+static float angle(const Vector2& point, const Vector2& center) {
     return atan2(point.y - center.y, point.x - center.x);
 }
 
@@ -51,7 +51,7 @@
  * @param p2 The second point defining the line segment
  * @return The distance along the ray if it intersects with the line segment, negative if otherwise
  */
-float rayIntersectPoints(const Vector2& rayOrigin, float dx, float dy,
+static float rayIntersectPoints(const Vector2& rayOrigin, float dx, float dy,
         const Vector2& p1, const Vector2& p2) {
     // The math below is derived from solving this formula, basically the
     // intersection point should stay on both the ray and the edge of (p1, p2).
@@ -548,14 +548,9 @@
     // Validate input, receiver is always at z = 0 plane.
     bool inputPolyPositionValid = true;
     for (int i = 0; i < polyLength; i++) {
-        if (poly[i].z <= 0.00001) {
-            inputPolyPositionValid = false;
-            ALOGE("polygon below the surface");
-            break;
-        }
         if (poly[i].z >= lightPoly[0].z) {
             inputPolyPositionValid = false;
-            ALOGE("polygon above the light");
+            ALOGW("polygon above the light");
             break;
         }
     }
diff --git a/libs/hwui/Vector.h b/libs/hwui/Vector.h
index 15b9d6b..c61cb61 100644
--- a/libs/hwui/Vector.h
+++ b/libs/hwui/Vector.h
@@ -124,6 +124,10 @@
     Vector3(float px, float py, float pz) :
         x(px), y(py), z(pz) {
     }
+
+    void dump() {
+        ALOGD("Vector3[%.2f, %.2f, %.2f]", x, y, z);
+    }
 };
 
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index ce66d8f..5ed9f1d 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -373,7 +373,7 @@
     mCanvas->setViewport(width, height);
 }
 
-void CanvasContext::setDisplayListData(DisplayList* displayList, DisplayListData* newData) {
+void CanvasContext::setDisplayListData(RenderNode* displayList, DisplayListData* newData) {
     displayList->setData(newData);
 }
 
@@ -388,7 +388,7 @@
     }
 }
 
-void CanvasContext::drawDisplayList(DisplayList* displayList, Rect* dirty) {
+void CanvasContext::drawDisplayList(RenderNode* displayList, Rect* dirty) {
     LOG_ALWAYS_FATAL_IF(!mCanvas || mEglSurface == EGL_NO_SURFACE,
             "drawDisplayList called on a context with no canvas or surface!");
 
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 649ffb6..e3fdf97 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -31,7 +31,7 @@
 namespace uirenderer {
 
 class DeferredLayerUpdater;
-class DisplayList;
+class RenderNode;
 class DisplayListData;
 class OpenGLRenderer;
 class Rect;
@@ -63,9 +63,9 @@
     bool initialize(EGLNativeWindowType window);
     void updateSurface(EGLNativeWindowType window);
     void setup(int width, int height);
-    void setDisplayListData(DisplayList* displayList, DisplayListData* newData);
+    void setDisplayListData(RenderNode* displayList, DisplayListData* newData);
     void processLayerUpdates(const Vector<DeferredLayerUpdater*>* layerUpdaters);
-    void drawDisplayList(DisplayList* displayList, Rect* dirty);
+    void drawDisplayList(RenderNode* displayList, Rect* dirty);
     void destroyCanvas();
 
     bool copyLayerInto(DeferredLayerUpdater* layer, SkBitmap* bitmap);
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 200c21f..93360fc 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -117,13 +117,13 @@
     post(task);
 }
 
-CREATE_BRIDGE3(setDisplayListData, CanvasContext* context, DisplayList* displayList,
+CREATE_BRIDGE3(setDisplayListData, CanvasContext* context, RenderNode* displayList,
         DisplayListData* newData) {
     args->context->setDisplayListData(args->displayList, args->newData);
     return NULL;
 }
 
-void RenderProxy::setDisplayListData(DisplayList* displayList, DisplayListData* newData) {
+void RenderProxy::setDisplayListData(RenderNode* displayList, DisplayListData* newData) {
     SETUP_TASK(setDisplayListData);
     args->context = mContext;
     args->displayList = displayList;
@@ -131,7 +131,7 @@
     post(task);
 }
 
-CREATE_BRIDGE4(drawDisplayList, CanvasContext* context, DisplayList* displayList,
+CREATE_BRIDGE4(drawDisplayList, CanvasContext* context, RenderNode* displayList,
         Rect dirty, const Vector<DeferredLayerUpdater*>* layerUpdates) {
     Rect* dirty = &args->dirty;
     if (dirty->bottom == -1 && dirty->left == -1 &&
@@ -143,7 +143,7 @@
     return NULL;
 }
 
-void RenderProxy::drawDisplayList(DisplayList* displayList,
+void RenderProxy::drawDisplayList(RenderNode* displayList,
         int dirtyLeft, int dirtyTop, int dirtyRight, int dirtyBottom) {
     SETUP_TASK(drawDisplayList);
     args->context = mContext;
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 83a8a8f..73e9805 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -32,7 +32,7 @@
 namespace uirenderer {
 
 class DeferredLayerUpdater;
-class DisplayList;
+class RenderNode;
 class DisplayListData;
 class Layer;
 class Rect;
@@ -60,8 +60,8 @@
     ANDROID_API bool initialize(EGLNativeWindowType window);
     ANDROID_API void updateSurface(EGLNativeWindowType window);
     ANDROID_API void setup(int width, int height);
-    ANDROID_API void setDisplayListData(DisplayList* displayList, DisplayListData* newData);
-    ANDROID_API void drawDisplayList(DisplayList* displayList,
+    ANDROID_API void setDisplayListData(RenderNode* displayList, DisplayListData* newData);
+    ANDROID_API void drawDisplayList(RenderNode* displayList,
             int dirtyLeft, int dirtyTop, int dirtyRight, int dirtyBottom);
     ANDROID_API void destroyCanvas();
 
diff --git a/media/java/android/media/Rating.java b/media/java/android/media/Rating.java
index b94db18..f4fbe2c 100644
--- a/media/java/android/media/Rating.java
+++ b/media/java/android/media/Rating.java
@@ -29,8 +29,13 @@
  * through one of the factory methods.
  */
 public final class Rating implements Parcelable {
-
     private final static String TAG = "Rating";
+    /**
+     * Indicates a rating style is not supported. A Rating will never have this
+     * type, but can be used by other classes to indicate they do not support
+     * Rating.
+     */
+    public final static int RATING_NONE = 0;
 
     /**
      * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to
diff --git a/media/java/android/media/session/IMediaController.aidl b/media/java/android/media/session/IMediaController.aidl
index 8ca0e45..d34e973 100644
--- a/media/java/android/media/session/IMediaController.aidl
+++ b/media/java/android/media/session/IMediaController.aidl
@@ -16,9 +16,12 @@
 package android.media.session;
 
 import android.content.Intent;
+import android.media.Rating;
 import android.media.session.IMediaControllerCallback;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
-import android.os.IBinder;
+import android.os.ResultReceiver;
 import android.view.KeyEvent;
 
 /**
@@ -26,9 +29,23 @@
  * @hide
  */
 interface IMediaController {
-    void sendCommand(String command, in Bundle extras);
+    void sendCommand(String command, in Bundle extras, in ResultReceiver cb);
     void sendMediaButton(in KeyEvent mediaButton);
     void registerCallbackListener(in IMediaControllerCallback cb);
     void unregisterCallbackListener(in IMediaControllerCallback cb);
-    int getPlaybackState();
+    boolean isTransportControlEnabled();
+
+    // These commands are for the TransportController
+    void play();
+    void pause();
+    void stop();
+    void next();
+    void previous();
+    void fastForward();
+    void rewind();
+    void seekTo(long pos);
+    void rate(in Rating rating);
+    MediaMetadata getMetadata();
+    PlaybackState getPlaybackState();
+    int getRatingType();
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/IMediaControllerCallback.aidl b/media/java/android/media/session/IMediaControllerCallback.aidl
index 3aa0ee4..3651f1b 100644
--- a/media/java/android/media/session/IMediaControllerCallback.aidl
+++ b/media/java/android/media/session/IMediaControllerCallback.aidl
@@ -15,6 +15,8 @@
 
 package android.media.session;
 
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 
 /**
@@ -22,7 +24,9 @@
  */
 oneway interface IMediaControllerCallback {
     void onEvent(String event, in Bundle extras);
-    void onMetadataUpdate(in Bundle metadata);
-    void onPlaybackUpdate(int newState);
     void onRouteChanged(in Bundle route);
+
+    // These callbacks are for the TransportController
+    void onPlaybackStateChanged(in PlaybackState state);
+    void onMetadataChanged(in MediaMetadata metadata);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/IMediaSession.aidl b/media/java/android/media/session/IMediaSession.aidl
index 19f7092..aed7641 100644
--- a/media/java/android/media/session/IMediaSession.aidl
+++ b/media/java/android/media/session/IMediaSession.aidl
@@ -16,6 +16,8 @@
 package android.media.session;
 
 import android.media.session.IMediaController;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 
 /**
@@ -23,11 +25,17 @@
  * @hide
  */
 interface IMediaSession {
-    void sendEvent(in Bundle data);
-    IMediaController getMediaSessionToken();
-    void setPlaybackState(int state);
-    void setMetadata(in Bundle metadata);
+    void sendEvent(String event, in Bundle data);
+    IMediaController getMediaController();
+    void setTransportPerformerEnabled();
     void setRouteState(in Bundle routeState);
     void setRoute(in Bundle mediaRouteDescriptor);
+    List<String> getSupportedInterfaces();
+    void publish();
     void destroy();
+
+    // These commands are for the TransportPerformer
+    void setMetadata(in MediaMetadata metadata);
+    void setPlaybackState(in PlaybackState state);
+    void setRatingType(int type);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/IMediaSessionCallback.aidl b/media/java/android/media/session/IMediaSessionCallback.aidl
index eb5f222..7c183e0 100644
--- a/media/java/android/media/session/IMediaSessionCallback.aidl
+++ b/media/java/android/media/session/IMediaSessionCallback.aidl
@@ -15,15 +15,27 @@
 
 package android.media.session;
 
+import android.media.Rating;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.IBinder;
+import android.os.ResultReceiver;
 
 /**
  * @hide
  */
 oneway interface IMediaSessionCallback {
-    void onCommand(String command, in Bundle extras);
+    void onCommand(String command, in Bundle extras, in ResultReceiver cb);
     void onMediaButton(in Intent mediaRequestIntent);
     void onRequestRouteChange(in Bundle route);
+
+    // These callbacks are for the TransportPerformer
+    void onPlay();
+    void onPause();
+    void onStop();
+    void onNext();
+    void onPrevious();
+    void onFastForward();
+    void onRewind();
+    void onSeekTo(long pos);
+    void onRate(in Rating rating);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java
index 09de859..afd8b11 100644
--- a/media/java/android/media/session/MediaController.java
+++ b/media/java/android/media/session/MediaController.java
@@ -16,20 +16,17 @@
 
 package android.media.session;
 
-import android.content.Intent;
-import android.media.session.IMediaController;
-import android.media.session.IMediaControllerCallback;
-import android.media.MediaMetadataRetriever;
-import android.media.RemoteControlClient;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
 /**
@@ -46,58 +43,62 @@
 public final class MediaController {
     private static final String TAG = "MediaController";
 
-    private static final int MESSAGE_EVENT = 1;
+    private static final int MSG_EVENT = 1;
     private static final int MESSAGE_PLAYBACK_STATE = 2;
     private static final int MESSAGE_METADATA = 3;
-    private static final int MESSAGE_ROUTE = 4;
-
-    private static final String KEY_EVENT = "event";
-    private static final String KEY_EXTRAS = "extras";
+    private static final int MSG_ROUTE = 4;
 
     private final IMediaController mSessionBinder;
 
-    private final CallbackStub mCbStub = new CallbackStub();
-    private final ArrayList<Callback> mCbs = new ArrayList<Callback>();
+    private final CallbackStub mCbStub = new CallbackStub(this);
+    private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
     private final Object mLock = new Object();
 
     private boolean mCbRegistered = false;
 
-    /**
-     * If you have a {@link MediaSessionToken} from the owner of the session a
-     * controller can be created directly. It is up to the session creator to
-     * handle token distribution if desired.
-     *
-     * @see MediaSession#getSessionToken()
-     * @param token A token from the creator of the session
-     */
-    public MediaController(MediaSessionToken token) {
-        mSessionBinder = token.getBinder();
+    private TransportController mTransportController;
+
+    private MediaController(IMediaController sessionBinder) {
+        mSessionBinder = sessionBinder;
     }
 
     /**
      * @hide
      */
-    public MediaController(IMediaController sessionBinder) {
-        mSessionBinder = sessionBinder;
+    public static MediaController fromBinder(IMediaController sessionBinder) {
+        MediaController controller = new MediaController(sessionBinder);
+        try {
+            controller.mSessionBinder.registerCallbackListener(controller.mCbStub);
+            if (controller.mSessionBinder.isTransportControlEnabled()) {
+                controller.mTransportController = new TransportController(sessionBinder);
+            }
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "MediaController created with expired token", e);
+            controller = null;
+        }
+        return controller;
     }
 
     /**
-     * Sends a generic command to the session. It is up to the session creator
-     * to decide what commands and parameters they will support. As such,
-     * commands should only be sent to sessions that the controller owns.
+     * Get a new MediaController for a MediaSessionToken. If successful the
+     * controller returned will be connected to the session that generated the
+     * token.
      *
-     * @param command The command to send
-     * @param params Any parameters to include with the command
+     * @param token The session token to use
+     * @return A controller for the session or null
      */
-    public void sendCommand(String command, Bundle params) {
-        if (TextUtils.isEmpty(command)) {
-            throw new IllegalArgumentException("command cannot be null or empty");
-        }
-        try {
-            mSessionBinder.sendCommand(command, params);
-        } catch (RemoteException e) {
-            Log.d(TAG, "Dead object in sendCommand.", e);
-        }
+    public static MediaController fromToken(MediaSessionToken token) {
+        return fromBinder(token.getBinder());
+    }
+
+    /**
+     * Get a TransportController if the session supports it. If it is not
+     * supported null will be returned.
+     *
+     * @return A TransportController or null
+     */
+    public TransportController getTransportController() {
+        return mTransportController;
     }
 
     /**
@@ -133,10 +134,10 @@
 
     /**
      * Adds a callback to receive updates from the session. Updates will be
-     * posted on the specified handler.
+     * posted on the specified handler's thread.
      *
      * @param cb Cannot be null.
-     * @param handler The handler to post updates on, if null the callers thread
+     * @param handler The handler to post updates on. If null the callers thread
      *            will be used
      */
     public void addCallback(Callback cb, Handler handler) {
@@ -160,6 +161,26 @@
         }
     }
 
+    /**
+     * Sends a generic command to the session. It is up to the session creator
+     * to decide what commands and parameters they will support. As such,
+     * commands should only be sent to sessions that the controller owns.
+     *
+     * @param command The command to send
+     * @param params Any parameters to include with the command
+     * @param cb The callback to receive the result on
+     */
+    public void sendCommand(String command, Bundle params, ResultReceiver cb) {
+        if (TextUtils.isEmpty(command)) {
+            throw new IllegalArgumentException("command cannot be null or empty");
+        }
+        try {
+            mSessionBinder.sendCommand(command, params, cb);
+        } catch (RemoteException e) {
+            Log.d(TAG, "Dead object in sendCommand.", e);
+        }
+    }
+
     /*
      * @hide
      */
@@ -174,14 +195,13 @@
         if (handler == null) {
             throw new IllegalArgumentException("Handler cannot be null");
         }
-        if (mCbs.contains(cb)) {
+        if (getHandlerForCallbackLocked(cb) != null) {
             Log.w(TAG, "Callback is already added, ignoring");
             return;
         }
-        cb.setHandler(handler);
-        mCbs.add(cb);
+        MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
+        mCallbacks.add(holder);
 
-        // Only register one cb binder, track callbacks internally and notify
         if (!mCbRegistered) {
             try {
                 mSessionBinder.registerCallbackListener(mCbStub);
@@ -192,56 +212,58 @@
         }
     }
 
-    private void removeCallbackLocked(Callback cb) {
+    private boolean removeCallbackLocked(Callback cb) {
         if (cb == null) {
             throw new IllegalArgumentException("Callback cannot be null");
         }
-        mCbs.remove(cb);
-
-        if (mCbs.size() == 0 && mCbRegistered) {
-            try {
-                mSessionBinder.unregisterCallbackListener(mCbStub);
-            } catch (RemoteException e) {
-                Log.d(TAG, "Dead object in unregisterCallback", e);
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                mCallbacks.remove(i);
+                return true;
             }
-            mCbRegistered = false;
+        }
+        return false;
+    }
+
+    private MessageHandler getHandlerForCallbackLocked(Callback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Callback cannot be null");
+        }
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private void postEvent(String event, Bundle extras) {
+        synchronized (mLock) {
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                mCallbacks.get(i).post(MSG_EVENT, event, extras);
+            }
         }
     }
 
-    private void pushOnEventLocked(String event, Bundle extras) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postEvent(event, extras);
-        }
-    }
-
-    private void pushOnMetadataUpdateLocked(Bundle metadata) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postMetadataUpdate(metadata);
-        }
-    }
-
-    private void pushOnPlaybackUpdateLocked(int newState) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postPlaybackStateChange(newState);
-        }
-    }
-
-    private void pushOnRouteChangedLocked(Bundle routeDescriptor) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postRouteChanged(routeDescriptor);
+    private void postRouteChanged(Bundle routeDescriptor) {
+        synchronized (mLock) {
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                mCallbacks.get(i).post(MSG_ROUTE, null, routeDescriptor);
+            }
         }
     }
 
     /**
-     * MediaSession callbacks will be posted on the thread that created the
-     * Callback object.
+     * Callback for receiving updates on from the session. A Callback can be
+     * registered using {@link #addCallback}
      */
     public static abstract class Callback {
-        private Handler mHandler;
-
         /**
-         * Override to handle custom events sent by the session owner.
-         * Controllers should only handle these for sessions they own.
+         * Override to handle custom events sent by the session owner without a
+         * specified interface. Controllers should only handle these for
+         * sessions they own.
          *
          * @param event
          */
@@ -249,119 +271,83 @@
         }
 
         /**
-         * Override to handle updates to the playback state. Valid values are in
-         * {@link RemoteControlClient}. TODO put playstate values somewhere more
-         * generic.
-         *
-         * @param state
-         */
-        public void onPlaybackStateChange(int state) {
-        }
-
-        /**
-         * Override to handle metadata changes for this session's media. The
-         * default supported fields are those in {@link MediaMetadataRetriever}.
-         *
-         * @param metadata
-         */
-        public void onMetadataUpdate(Bundle metadata) {
-        }
-
-        /**
          * Override to handle route changes for this session.
          *
          * @param route
          */
         public void onRouteChanged(Bundle route) {
         }
-
-        private void setHandler(Handler handler) {
-            mHandler = new MessageHandler(handler.getLooper(), this);
-        }
-
-        private void postEvent(String event, Bundle extras) {
-            Bundle eventBundle = new Bundle();
-            eventBundle.putString(KEY_EVENT, event);
-            eventBundle.putBundle(KEY_EXTRAS, extras);
-            Message msg = mHandler.obtainMessage(MESSAGE_EVENT, eventBundle);
-            mHandler.sendMessage(msg);
-        }
-
-        private void postPlaybackStateChange(final int state) {
-            Message msg = mHandler.obtainMessage(MESSAGE_PLAYBACK_STATE, state, 0);
-            mHandler.sendMessage(msg);
-        }
-
-        private void postMetadataUpdate(final Bundle metadata) {
-            Message msg = mHandler.obtainMessage(MESSAGE_METADATA, metadata);
-            mHandler.sendMessage(msg);
-        }
-
-        private void postRouteChanged(final Bundle descriptor) {
-            Message msg = mHandler.obtainMessage(MESSAGE_ROUTE, descriptor);
-            mHandler.sendMessage(msg);
-        }
     }
 
-    private final class CallbackStub extends IMediaControllerCallback.Stub {
+    private final static class CallbackStub extends IMediaControllerCallback.Stub {
+        private final WeakReference<MediaController> mController;
+
+        public CallbackStub(MediaController controller) {
+            mController = new WeakReference<MediaController>(controller);
+        }
 
         @Override
-        public void onEvent(String event, Bundle extras) throws RemoteException {
-            synchronized (mLock) {
-                pushOnEventLocked(event, extras);
+        public void onEvent(String event, Bundle extras) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                controller.postEvent(event, extras);
             }
         }
 
         @Override
-        public void onMetadataUpdate(Bundle metadata) throws RemoteException {
-            synchronized (mLock) {
-                pushOnMetadataUpdateLocked(metadata);
+        public void onRouteChanged(Bundle mediaRouteDescriptor) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                controller.postRouteChanged(mediaRouteDescriptor);
             }
         }
 
         @Override
-        public void onPlaybackUpdate(final int newState) throws RemoteException {
-            synchronized (mLock) {
-                pushOnPlaybackUpdateLocked(newState);
+        public void onPlaybackStateChanged(PlaybackState state) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                TransportController tc = controller.getTransportController();
+                if (tc != null) {
+                    tc.postPlaybackStateChanged(state);
+                }
             }
         }
 
         @Override
-        public void onRouteChanged(Bundle mediaRouteDescriptor) throws RemoteException {
-            synchronized (mLock) {
-                pushOnRouteChangedLocked(mediaRouteDescriptor);
+        public void onMetadataChanged(MediaMetadata metadata) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                TransportController tc = controller.getTransportController();
+                if (tc != null) {
+                    tc.postMetadataChanged(metadata);
+                }
             }
         }
 
     }
 
     private final static class MessageHandler extends Handler {
-        private final MediaController.Callback mCb;
+        private final MediaController.Callback mCallback;
 
         public MessageHandler(Looper looper, MediaController.Callback cb) {
-            super(looper);
-            mCb = cb;
+            super(looper, null, true);
+            mCallback = cb;
         }
 
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MESSAGE_EVENT:
-                    Bundle eventBundle = (Bundle) msg.obj;
-                    String event = eventBundle.getString(KEY_EVENT);
-                    Bundle extras = eventBundle.getBundle(KEY_EXTRAS);
-                    mCb.onEvent(event, extras);
+                case MSG_EVENT:
+                    mCallback.onEvent((String) msg.obj, msg.getData());
                     break;
-                case MESSAGE_PLAYBACK_STATE:
-                    mCb.onPlaybackStateChange(msg.arg1);
-                    break;
-                case MESSAGE_METADATA:
-                    mCb.onMetadataUpdate((Bundle) msg.obj);
-                    break;
-                case MESSAGE_ROUTE:
-                    mCb.onRouteChanged((Bundle) msg.obj);
+                case MSG_ROUTE:
+                    mCallback.onRouteChanged(msg.getData());
             }
         }
+
+        public void post(int what, Object obj, Bundle data) {
+            obtainMessage(what, obj).sendToTarget();
+        }
     }
 
 }
diff --git a/media/java/android/media/session/MediaMetadata.aidl b/media/java/android/media/session/MediaMetadata.aidl
new file mode 100644
index 0000000..4431d9d
--- /dev/null
+++ b/media/java/android/media/session/MediaMetadata.aidl
@@ -0,0 +1,18 @@
+/* Copyright 2014, 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.session;
+
+parcelable MediaMetadata;
diff --git a/media/java/android/media/session/MediaMetadata.java b/media/java/android/media/session/MediaMetadata.java
new file mode 100644
index 0000000..e2330f7
--- /dev/null
+++ b/media/java/android/media/session/MediaMetadata.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2014 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.session;
+
+import android.graphics.Bitmap;
+import android.media.Rating;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.Log;
+
+/**
+ * Contains metadata about an item, such as the title, artist, etc.
+ */
+public final class MediaMetadata implements Parcelable {
+    private static final String TAG = "MediaMetadata";
+
+    /**
+     * The title of the media.
+     */
+    public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+
+    /**
+     * The artist of the media.
+     */
+    public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+
+    /**
+     * The duration of the media in ms. A duration of 0 is the default.
+     */
+    public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+
+    /**
+     * The album title for the media.
+     */
+    public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+
+    /**
+     * The author of the media.
+     */
+    public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+    /**
+     * The writer of the media.
+     */
+    public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+
+    /**
+     * The composer of the media.
+     */
+    public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+    /**
+     * The date the media was created or published as TODO determine format.
+     */
+    public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
+
+    /**
+     * The year the media was created or published as a numeric String.
+     */
+    public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+
+    /**
+     * The genre of the media.
+     */
+    public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+
+    /**
+     * The track number for the media.
+     */
+    public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+    /**
+     * The number of tracks in the media's original source.
+     */
+    public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+
+    /**
+     * The disc number for the media's original source.
+     */
+    public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+    /**
+     * The artist for the album of the media's original source.
+     */
+    public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+    /**
+     * The artwork for the media as a {@link Bitmap}.
+     */
+    public static final String METADATA_KEY_ART = "android.media.metadata.ART";
+
+    /**
+     * The artwork for the media as a Uri style String.
+     */
+    public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+
+    /**
+     * The artwork for the album of the media's original source as a
+     * {@link Bitmap}.
+     */
+    public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+
+    /**
+     * The artwork for the album of the media's original source as a Uri style
+     * String.
+     */
+    public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+
+    /**
+     * The user's rating for the media.
+     *
+     * @see Rating
+     */
+    public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+
+    /**
+     * The overall rating for the media.
+     *
+     * @see Rating
+     */
+    public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
+
+    private static final int METADATA_TYPE_INVALID = -1;
+    private static final int METADATA_TYPE_LONG = 0;
+    private static final int METADATA_TYPE_STRING = 1;
+    private static final int METADATA_TYPE_BITMAP = 2;
+    private static final int METADATA_TYPE_RATING = 3;
+    private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
+
+    static {
+        METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
+        METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
+    }
+    private final Bundle mBundle;
+
+    private MediaMetadata(Bundle bundle) {
+        mBundle = new Bundle(bundle);
+    }
+
+    private MediaMetadata(Parcel in) {
+        mBundle = in.readBundle();
+    }
+
+    /**
+     * Returns the value associated with the given key, or null if no mapping of
+     * the desired type exists for the given key or a null value is explicitly
+     * associated with the key.
+     *
+     * @param key The key the value is stored under
+     * @return a String value, or null
+     */
+    public String getString(String key) {
+        return mBundle.getString(key);
+    }
+
+    /**
+     * Returns the value associated with the given key, or 0L if no long exists
+     * for the given key.
+     *
+     * @param key The key the value is stored under
+     * @return a long value
+     */
+    public long getLong(String key) {
+        return mBundle.getLong(key);
+    }
+
+    /**
+     * Return a {@link Rating} for the given key or null if no rating exists for
+     * the given key.
+     *
+     * @param key The key the value is stored under
+     * @return A {@link Rating} or null
+     */
+    public Rating getRating(String key) {
+        Rating rating = null;
+        try {
+            rating = mBundle.getParcelable(key);
+        } catch (Exception e) {
+            // ignore, value was not a bitmap
+            Log.d(TAG, "Failed to retrieve a key as Rating.", e);
+        }
+        return rating;
+    }
+
+    /**
+     * Return a {@link Bitmap} for the given key or null if no bitmap exists for
+     * the given key.
+     *
+     * @param key The key the value is stored under
+     * @return A {@link Bitmap} or null
+     */
+    public Bitmap getBitmap(String key) {
+        Bitmap bmp = null;
+        try {
+            bmp = mBundle.getParcelable(key);
+        } catch (Exception e) {
+            // ignore, value was not a bitmap
+            Log.d(TAG, "Failed to retrieve a key as Bitmap.", e);
+        }
+        return bmp;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeBundle(mBundle);
+    }
+
+    public static final Parcelable.Creator<MediaMetadata> CREATOR
+            = new Parcelable.Creator<MediaMetadata>() {
+                @Override
+                public MediaMetadata createFromParcel(Parcel in) {
+                    return new MediaMetadata(in);
+                }
+
+                @Override
+                public MediaMetadata[] newArray(int size) {
+                    return new MediaMetadata[size];
+                }
+            };
+
+    /**
+     * Use to build MediaMetadata objects. The system defined metadata keys must
+     * use the appropriate data type.
+     */
+    public static final class Builder {
+        private final Bundle mBundle;
+
+        /**
+         * Create an empty Builder. Any field that should be included in the
+         * {@link MediaMetadata} must be added.
+         */
+        public Builder() {
+            mBundle = new Bundle();
+        }
+
+        /**
+         * Create a Builder using a {@link MediaMetadata} instance to set the
+         * initial values. All fields in the source metadata will be included in
+         * the new metadata. Fields can be overwritten by adding the same key.
+         *
+         * @param source
+         */
+        public Builder(MediaMetadata source) {
+            mBundle = new Bundle(source.mBundle);
+        }
+
+        /**
+         * Put a String value into the metadata. Custom keys may be used, but if
+         * the METADATA_KEYs defined in this class are used they may only be one
+         * of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_TITLE}</li>
+         * <li>{@link #METADATA_KEY_ARTIST}</li>
+         * <li>{@link #METADATA_KEY_ALBUM}</li>
+         * <li>{@link #METADATA_KEY_AUTHOR}</li>
+         * <li>{@link #METADATA_KEY_WRITER}</li>
+         * <li>{@link #METADATA_KEY_COMPOSER}</li>
+         * <li>{@link #METADATA_KEY_DATE}</li>
+         * <li>{@link #METADATA_KEY_YEAR}</li>
+         * <li>{@link #METADATA_KEY_GENRE}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>li>
+         * <li>{@link #METADATA_KEY_ART_URI}</li>li>
+         * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putString(String key, String value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_STRING) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a String");
+                }
+            }
+            mBundle.putString(key, value);
+            return this;
+        }
+
+        /**
+         * Put a long value into the metadata. Custom keys may be used, but if
+         * the METADATA_KEYs defined in this class are used they may only be one
+         * of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_DURATION}</li>
+         * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
+         * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
+         * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putLong(String key, long value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a long");
+                }
+            }
+            mBundle.putLong(key, value);
+            return this;
+        }
+
+        /**
+         * Put a {@link Rating} into the metadata. Custom keys may be used, but
+         * if the METADATA_KEYs defined in this class are used they may only be
+         * one of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_RATING}</li>
+         * <li>{@link #METADATA_KEY_USER_RATING}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putRating(String key, Rating value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a Rating");
+                }
+            }
+            mBundle.putParcelable(key, value);
+            return this;
+        }
+
+        /**
+         * Put a {@link Bitmap} into the metadata. Custom keys may be used, but
+         * if the METADATA_KEYs defined in this class are used they may only be
+         * one of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_ART}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ART}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The Bitmap to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putBitmap(String key, Bitmap value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a Bitmap");
+                }
+            }
+            mBundle.putParcelable(key, value);
+            return this;
+        }
+
+        /**
+         * Creates a {@link MediaMetadata} instance with the specified fields.
+         *
+         * @return The new MediaMetadata instance
+         */
+        public MediaMetadata build() {
+            return new MediaMetadata(mBundle);
+        }
+    }
+
+}
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 1f1533b..23c3035 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -17,17 +17,21 @@
 package android.media.session;
 
 import android.content.Intent;
+import android.media.Rating;
 import android.media.session.IMediaController;
 import android.media.session.IMediaSession;
 import android.media.session.IMediaSessionCallback;
-import android.media.RemoteControlClient;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
 /**
@@ -58,12 +62,13 @@
 public final class MediaSession {
     private static final String TAG = "MediaSession";
 
-    private static final int MESSAGE_MEDIA_BUTTON = 1;
-    private static final int MESSAGE_COMMAND = 2;
-    private static final int MESSAGE_ROUTE_CHANGE = 3;
+    private static final int MSG_MEDIA_BUTTON = 1;
+    private static final int MSG_COMMAND = 2;
+    private static final int MSG_ROUTE_CHANGE = 3;
 
     private static final String KEY_COMMAND = "command";
     private static final String KEY_EXTRAS = "extras";
+    private static final String KEY_CALLBACK = "callback";
 
     private final Object mLock = new Object();
 
@@ -71,7 +76,14 @@
     private final IMediaSession mBinder;
     private final CallbackStub mCbStub;
 
-    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+    private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
+    // TODO route interfaces
+    private final ArrayMap<String, RouteInterface.Stub> mInterfaces
+            = new ArrayMap<String, RouteInterface.Stub>();
+
+    private TransportPerformer mPerformer;
+
+    private boolean mPublished = false;;
 
     /**
      * @hide
@@ -81,7 +93,7 @@
         mCbStub = cbStub;
         IMediaController controllerBinder = null;
         try {
-            controllerBinder = mBinder.getMediaSessionToken();
+            controllerBinder = mBinder.getMediaController();
         } catch (RemoteException e) {
             throw new RuntimeException("Dead object in MediaSessionController constructor: ", e);
         }
@@ -102,34 +114,117 @@
             throw new IllegalArgumentException("Callback cannot be null");
         }
         synchronized (mLock) {
-            if (mCallbacks.contains(callback)) {
+            if (getHandlerForCallbackLocked(callback) != null) {
                 Log.w(TAG, "Callback is already added, ignoring");
+                return;
             }
             if (handler == null) {
                 handler = new Handler();
             }
             MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback);
-            callback.setHandler(msgHandler);
-            mCallbacks.add(callback);
+            mCallbacks.add(msgHandler);
         }
     }
 
     public void removeCallback(Callback callback) {
-        mCallbacks.remove(callback);
+        synchronized (mLock) {
+            removeCallbackLocked(callback);
+        }
     }
 
     /**
-     * Publish the current playback state to the system and any controllers.
-     * Valid values are defined in {@link RemoteControlClient}. TODO move play
-     * states somewhere else.
+     * Start using a TransportPerformer with this media session. This must be
+     * called before calling publish and cannot be called more than once.
+     * Calling this will allow MediaControllers to retrieve a
+     * TransportController.
      *
-     * @param state
+     * @see TransportController
+     * @return The TransportPerformer created for this session
      */
-    public void setPlaybackState(int state) {
+    public TransportPerformer setTransportPerformerEnabled() {
+        if (mPerformer != null) {
+            throw new IllegalStateException("setTransportPerformer can only be called once.");
+        }
+        if (mPublished) {
+            throw new IllegalStateException("setTransportPerformer cannot be called after publish");
+        }
+
+        mPerformer = new TransportPerformer(mBinder);
         try {
-            mBinder.setPlaybackState(state);
+            mBinder.setTransportPerformerEnabled();
         } catch (RemoteException e) {
-            Log.e(TAG, "Dead object in setPlaybackState: ", e);
+            Log.wtf(TAG, "Failure in setTransportPerformerEnabled.", e);
+        }
+        return mPerformer;
+    }
+
+    /**
+     * Retrieves the TransportPerformer used by this session. If called before
+     * {@link #setTransportPerformerEnabled} null will be returned.
+     *
+     * @return The TransportPerformer associated with this session or null
+     */
+    public TransportPerformer getTransportPerformer() {
+        return mPerformer;
+    }
+
+    /**
+     * Call after you have finished setting up the session. This will make it
+     * available to listeners and begin pushing updates to MediaControllers.
+     * This can only be called once.
+     */
+    public void publish() {
+        if (mPublished) {
+            throw new RuntimeException("publish() may only be called once.");
+        }
+        try {
+            mBinder.publish();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Failure in publish.", e);
+        }
+        mPublished = true;
+    }
+
+    /**
+     * Add an interface that can be used by MediaSessions. TODO make this a
+     * route provider api
+     *
+     * @see RouteInterface
+     * @param iface The interface to add
+     * @hide
+     */
+    public void addInterface(RouteInterface.Stub iface) {
+        if (iface == null) {
+            throw new IllegalArgumentException("Stub cannot be null");
+        }
+        String name = iface.getName();
+        if (TextUtils.isEmpty(name)) {
+            throw new IllegalArgumentException("Stub must return a valid name");
+        }
+        if (mInterfaces.containsKey(iface)) {
+            throw new IllegalArgumentException("Interface is already added");
+        }
+        synchronized (mLock) {
+            mInterfaces.put(iface.getName(), iface);
+        }
+    }
+
+    /**
+     * Send a proprietary event to all MediaControllers listening to this
+     * Session. It's up to the Controller/Session owner to determine the meaning
+     * of any events.
+     *
+     * @param event The name of the event to send
+     * @param extras Any extras included with the event
+     */
+    public void sendEvent(String event, Bundle extras) {
+        if (TextUtils.isEmpty(event)) {
+            throw new IllegalArgumentException("event cannot be null or empty");
+        }
+        try {
+            mBinder.sendEvent(event, extras);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error sending event", e);
         }
     }
 
@@ -142,7 +237,7 @@
         try {
             mBinder.destroy();
         } catch (RemoteException e) {
-            Log.e(TAG, "Dead object in onDestroy: ", e);
+            Log.wtf(TAG, "Error releasing session: ", e);
         }
     }
 
@@ -158,15 +253,38 @@
         return mSessionToken;
     }
 
-    private void postCommand(String command, Bundle extras) {
-        Bundle commandBundle = new Bundle();
-        commandBundle.putString(KEY_COMMAND, command);
-        commandBundle.putBundle(KEY_EXTRAS, extras);
+    private MessageHandler getHandlerForCallbackLocked(Callback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Callback cannot be null");
+        }
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private boolean removeCallbackLocked(Callback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Callback cannot be null");
+        }
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                mCallbacks.remove(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void postCommand(String command, Bundle extras, ResultReceiver resultCb) {
+        Command cmd = new Command(command, extras, resultCb);
         synchronized (mLock) {
             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                Callback cb = mCallbacks.get(i);
-                Message msg = cb.mHandler.obtainMessage(MESSAGE_COMMAND, commandBundle);
-                cb.mHandler.sendMessage(msg);
+                mCallbacks.get(i).post(MSG_COMMAND, cmd);
             }
         }
     }
@@ -174,9 +292,7 @@
     private void postMediaButton(Intent mediaButtonIntent) {
         synchronized (mLock) {
             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                Callback cb = mCallbacks.get(i);
-                Message msg = cb.mHandler.obtainMessage(MESSAGE_MEDIA_BUTTON, mediaButtonIntent);
-                cb.mHandler.sendMessage(msg);
+                mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent);
             }
         }
     }
@@ -184,9 +300,7 @@
     private void postRequestRouteChange(Bundle mediaRouteDescriptor) {
         synchronized (mLock) {
             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                Callback cb = mCallbacks.get(i);
-                Message msg = cb.mHandler.obtainMessage(MESSAGE_ROUTE_CHANGE, mediaRouteDescriptor);
-                cb.mHandler.sendMessage(msg);
+                mCallbacks.get(i).post(MSG_ROUTE_CHANGE, mediaRouteDescriptor);
             }
         }
     }
@@ -197,7 +311,6 @@
      * MediaSession (TODO).
      */
     public abstract static class Callback {
-        private MessageHandler mHandler;
 
         public Callback() {
         }
@@ -225,7 +338,7 @@
          * @param command
          * @param extras optional
          */
-        public void onCommand(String command, Bundle extras) {
+        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
         }
 
         /**
@@ -237,35 +350,140 @@
          */
         public void onRequestRouteChange(Bundle descriptor) {
         }
-
-        private void setHandler(MessageHandler handler) {
-            mHandler = handler;
-        }
     }
 
     /**
      * @hide
      */
     public static class CallbackStub extends IMediaSessionCallback.Stub {
-        private MediaSession mMediaSession;
+        private WeakReference<MediaSession> mMediaSession;
 
         public void setMediaSession(MediaSession session) {
-            mMediaSession = session;
+            mMediaSession = new WeakReference<MediaSession>(session);
         }
 
         @Override
-        public void onCommand(String command, Bundle extras) throws RemoteException {
-            mMediaSession.postCommand(command, extras);
+        public void onCommand(String command, Bundle extras, ResultReceiver cb)
+                throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                session.postCommand(command, extras, cb);
+            }
         }
 
         @Override
         public void onMediaButton(Intent mediaButtonIntent) throws RemoteException {
-            mMediaSession.postMediaButton(mediaButtonIntent);
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                session.postMediaButton(mediaButtonIntent);
+            }
         }
 
         @Override
         public void onRequestRouteChange(Bundle mediaRouteDescriptor) throws RemoteException {
-            mMediaSession.postRequestRouteChange(mediaRouteDescriptor);
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                session.postRequestRouteChange(mediaRouteDescriptor);
+            }
+        }
+
+        @Override
+        public void onPlay() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onPlay();
+                }
+            }
+        }
+
+        @Override
+        public void onPause() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onPause();
+                }
+            }
+        }
+
+        @Override
+        public void onStop() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onStop();
+                }
+            }
+        }
+
+        @Override
+        public void onNext() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onNext();
+                }
+            }
+        }
+
+        @Override
+        public void onPrevious() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onPrevious();
+                }
+            }
+        }
+
+        @Override
+        public void onFastForward() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onFastForward();
+                }
+            }
+        }
+
+        @Override
+        public void onRewind() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onRewind();
+                }
+            }
+        }
+
+        @Override
+        public void onSeekTo(long pos) throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onSeekTo(pos);
+                }
+            }
+        }
+
+        @Override
+        public void onRate(Rating rating) throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onRate(rating);
+                }
+            }
         }
 
     }
@@ -274,7 +492,7 @@
         private MediaSession.Callback mCallback;
 
         public MessageHandler(Looper looper, MediaSession.Callback callback) {
-            super(looper);
+            super(looper, null, true);
             mCallback = callback;
         }
 
@@ -285,21 +503,35 @@
                     return;
                 }
                 switch (msg.what) {
-                    case MESSAGE_MEDIA_BUTTON:
+                    case MSG_MEDIA_BUTTON:
                         mCallback.onMediaButton((Intent) msg.obj);
                         break;
-                    case MESSAGE_COMMAND:
-                        Bundle commandBundle = (Bundle) msg.obj;
-                        String command = commandBundle.getString(KEY_COMMAND);
-                        Bundle extras = commandBundle.getBundle(KEY_EXTRAS);
-                        mCallback.onCommand(command, extras);
+                    case MSG_COMMAND:
+                        Command cmd = (Command) msg.obj;
+                        mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
                         break;
-                    case MESSAGE_ROUTE_CHANGE:
+                    case MSG_ROUTE_CHANGE:
                         mCallback.onRequestRouteChange((Bundle) msg.obj);
                         break;
                 }
             }
             msg.recycle();
         }
+
+        public void post(int what, Object obj) {
+            obtainMessage(what, obj).sendToTarget();
+        }
+    }
+
+    private static final class Command {
+        public final String command;
+        public final Bundle extras;
+        public final ResultReceiver stub;
+
+        public Command(String command, Bundle extras, ResultReceiver stub) {
+            this.command = command;
+            this.extras = extras;
+            this.stub = stub;
+        }
     }
 }
diff --git a/media/java/android/media/session/PlaybackState.aidl b/media/java/android/media/session/PlaybackState.aidl
new file mode 100644
index 0000000..0876ebd
--- /dev/null
+++ b/media/java/android/media/session/PlaybackState.aidl
@@ -0,0 +1,18 @@
+/* Copyright 2014, 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.session;
+
+parcelable PlaybackState;
diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java
new file mode 100644
index 0000000..b3506b3
--- /dev/null
+++ b/media/java/android/media/session/PlaybackState.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2014 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.session;
+
+import android.media.RemoteControlClient;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Playback state for a {@link MediaSession}. This includes a state like
+ * {@link PlaybackState#PLAYSTATE_PLAYING}, the current playback position,
+ * and the current control capabilities.
+ */
+public final class PlaybackState implements Parcelable {
+    /**
+     * Indicates this performer supports the stop command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_STOP = 1 << 0;
+
+    /**
+     * Indicates this performer supports the pause command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_PAUSE = 1 << 1;
+
+    /**
+     * Indicates this performer supports the play command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_PLAY = 1 << 2;
+
+    /**
+     * Indicates this performer supports the rewind command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_REWIND = 1 << 3;
+
+    /**
+     * Indicates this performer supports the previous command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_PREVIOUS_ITEM = 1 << 4;
+
+    /**
+     * Indicates this performer supports the next command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_NEXT_ITEM = 1 << 5;
+
+    /**
+     * Indicates this performer supports the fast forward command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_FASTFORWARD = 1 << 6;
+
+    /**
+     * Indicates this performer supports the set rating command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_RATING = 1 << 7;
+
+    /**
+     * Indicates this performer supports the seek to command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_SEEK_TO = 1 << 8;
+
+    /**
+     * This is the default playback state and indicates that no media has been
+     * added yet, or the performer has been reset and has no content to play.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_NONE = 0;
+
+    /**
+     * State indicating this item is currently stopped.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_STOPPED = 1;
+
+    /**
+     * State indicating this item is currently paused.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_PAUSED = 2;
+
+    /**
+     * State indicating this item is currently playing.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_PLAYING = 3;
+
+    /**
+     * State indicating this item is currently fast forwarding.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_FAST_FORWARDING = 4;
+
+    /**
+     * State indicating this item is currently rewinding.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_REWINDING = 5;
+
+    /**
+     * State indicating this item is currently buffering and will begin playing
+     * when enough data has buffered.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_BUFFERING = 6;
+
+    /**
+     * State indicating this item is currently in an error state. The error
+     * message should also be set when entering this state.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_ERROR = 7;
+
+    private int mState;
+    private long mPosition;
+    private long mBufferPosition;
+    private float mSpeed;
+    private long mCapabilities;
+    private String mErrorMessage;
+
+    /**
+     * Create an empty PlaybackState. At minimum a state and actions should be
+     * set before publishing a PlaybackState.
+     */
+    public PlaybackState() {
+    }
+
+    /**
+     * Create a new PlaybackState from an existing PlaybackState. All fields
+     * will be copied to the new state.
+     *
+     * @param from The PlaybackState to duplicate
+     */
+    public PlaybackState(PlaybackState from) {
+        this.setState(from.getState());
+        this.setPosition(from.getPosition());
+        this.setBufferPosition(from.getBufferPosition());
+        this.setSpeed(from.getSpeed());
+        this.setActions(from.getActions());
+        this.setErrorMessage(from.getErrorMessage());
+    }
+
+    private PlaybackState(Parcel in) {
+        this.setState(in.readInt());
+        this.setPosition(in.readLong());
+        this.setBufferPosition(in.readLong());
+        this.setSpeed(in.readFloat());
+        this.setActions(in.readLong());
+        this.setErrorMessage(in.readString());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(getState());
+        dest.writeLong(getPosition());
+        dest.writeLong(getBufferPosition());
+        dest.writeFloat(getSpeed());
+        dest.writeLong(getActions());
+        dest.writeString(getErrorMessage());
+    }
+
+    /**
+     * Get the current state of playback. One of the following:
+     * <ul>
+     * <li> {@link PlaybackState#PLAYSTATE_NONE}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_STOPPED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PLAYING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PAUSED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_FAST_FORWARDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_REWINDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_BUFFERING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_ERROR}</li>
+     */
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * Set the current state of playback. One of the following:
+     * <ul>
+     * <li> {@link PlaybackState#PLAYSTATE_NONE}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_STOPPED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PLAYING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PAUSED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_FAST_FORWARDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_REWINDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_BUFFERING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_ERROR}</li>
+     */
+    public void setState(int mState) {
+        this.mState = mState;
+    }
+
+    /**
+     * Get the current playback position in ms.
+     */
+    public long getPosition() {
+        return mPosition;
+    }
+
+    /**
+     * Set the current playback position in ms.
+     */
+    public void setPosition(long position) {
+        mPosition = position;
+    }
+
+    /**
+     * Get the current buffer position in ms. This is the farthest playback
+     * point that can be reached from the current position using only buffered
+     * content.
+     */
+    public long getBufferPosition() {
+        return mBufferPosition;
+    }
+
+    /**
+     * Set the current buffer position in ms. This is the farthest playback
+     * point that can be reached from the current position using only buffered
+     * content.
+     */
+    public void setBufferPosition(long bufferPosition) {
+        mBufferPosition = bufferPosition;
+    }
+
+    /**
+     * Get the current playback speed as a multiple of normal playback. This
+     * should be negative when rewinding. A value of 1 means normal playback and
+     * 0 means paused.
+     */
+    public float getSpeed() {
+        return mSpeed;
+    }
+
+    /**
+     * Set the current playback speed as a multiple of normal playback. This
+     * should be negative when rewinding. A value of 1 means normal playback and
+     * 0 means paused.
+     */
+    public void setSpeed(float speed) {
+        mSpeed = speed;
+    }
+
+    /**
+     * Get the current actions available on this session. This should use a
+     * bitmask of the available actions.
+     * <ul>
+     * <li> {@link PlaybackState#ACTION_PREVIOUS_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_REWIND}</li>
+     * <li> {@link PlaybackState#ACTION_PLAY}</li>
+     * <li> {@link PlaybackState#ACTION_PAUSE}</li>
+     * <li> {@link PlaybackState#ACTION_STOP}</li>
+     * <li> {@link PlaybackState#ACTION_FASTFORWARD}</li>
+     * <li> {@link PlaybackState#ACTION_NEXT_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_SEEK_TO}</li>
+     * <li> {@link PlaybackState#ACTION_RATING}</li>
+     * </ul>
+     */
+    public long getActions() {
+        return mCapabilities;
+    }
+
+    /**
+     * Set the current capabilities available on this session. This should use a
+     * bitmask of the available capabilities.
+     * <ul>
+     * <li> {@link PlaybackState#ACTION_PREVIOUS_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_REWIND}</li>
+     * <li> {@link PlaybackState#ACTION_PLAY}</li>
+     * <li> {@link PlaybackState#ACTION_PAUSE}</li>
+     * <li> {@link PlaybackState#ACTION_STOP}</li>
+     * <li> {@link PlaybackState#ACTION_FASTFORWARD}</li>
+     * <li> {@link PlaybackState#ACTION_NEXT_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_SEEK_TO}</li>
+     * <li> {@link PlaybackState#ACTION_RATING}</li>
+     * </ul>
+     */
+    public void setActions(long capabilities) {
+        mCapabilities = capabilities;
+    }
+
+    /**
+     * Get a user readable error message. This should be set when the state is
+     * {@link PlaybackState#PLAYSTATE_ERROR}.
+     */
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    /**
+     * Set a user readable error message. This should be set when the state is
+     * {@link PlaybackState#PLAYSTATE_ERROR}.
+     */
+    public void setErrorMessage(String errorMessage) {
+        mErrorMessage = errorMessage;
+    }
+
+    public static final Parcelable.Creator<PlaybackState> CREATOR
+            = new Parcelable.Creator<PlaybackState>() {
+        @Override
+        public PlaybackState createFromParcel(Parcel in) {
+            return new PlaybackState(in);
+        }
+
+        @Override
+        public PlaybackState[] newArray(int size) {
+            return new PlaybackState[size];
+        }
+    };
+}
diff --git a/media/java/android/media/session/RouteInterface.java b/media/java/android/media/session/RouteInterface.java
new file mode 100644
index 0000000..2391f27
--- /dev/null
+++ b/media/java/android/media/session/RouteInterface.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2014 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.session;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+
+/**
+ * Routes can support multiple interfaces for MediaSessions to interact with. To
+ * add a standard interface you should implement that interface's RouteInterface
+ * Stub and register it with the session. The set of supported commands is
+ * dependent on the specific interface's implementation.
+ * <p>
+ * A MediaInterface can be registered by calling TODO. Once added an interface
+ * will be used by Sessions to decide how they communicate with a session and
+ * cannot be removed, so all interfaces that you plan to support should be added
+ * when the route is created.
+ *
+ * @see RouteTransportControls
+ */
+public final class RouteInterface {
+    private static final String TAG = "MediaInterface";
+
+    private static final String KEY_RESULT = "result";
+
+    private final MediaController mController;
+    private final String mIface;
+
+    /**
+     * @hide
+     */
+    RouteInterface(MediaController controller, String iface) {
+        mController = controller;
+        mIface = iface;
+    }
+
+    public void sendCommand(String command, Bundle params, ResultReceiver cb) {
+        // TODO
+    }
+
+    public void addListener(EventListener listener) {
+        addListener(listener, null);
+    }
+
+    public void addListener(EventListener listener, Handler handler) {
+        // TODO See MediaController for add/remove pattern
+    }
+
+    public void removeListener(EventListener listener) {
+        // TODO
+    }
+
+    // TODO decide on list of supported types
+    private static Bundle writeResultToBundle(Object v) {
+        Bundle b = new Bundle();
+        if (v == null) {
+            // Don't send anything if null
+        } else if (v instanceof String) {
+            b.putString(KEY_RESULT, (String) v);
+        } else if (v instanceof Integer) {
+            b.putInt(KEY_RESULT, (Integer) v);
+        } else if (v instanceof Bundle) {
+            // Must be before Parcelable
+            b.putBundle(KEY_RESULT, (Bundle) v);
+        } else if (v instanceof Parcelable) {
+            b.putParcelable(KEY_RESULT, (Parcelable) v);
+        } else if (v instanceof Short) {
+            b.putShort(KEY_RESULT, (Short) v);
+        } else if (v instanceof Long) {
+            b.putLong(KEY_RESULT, (Long) v);
+        } else if (v instanceof Float) {
+            b.putFloat(KEY_RESULT, (Float) v);
+        } else if (v instanceof Double) {
+            b.putDouble(KEY_RESULT, (Double) v);
+        } else if (v instanceof Boolean) {
+            b.putBoolean(KEY_RESULT, (Boolean) v);
+        } else if (v instanceof CharSequence) {
+            // Must be after String
+            b.putCharSequence(KEY_RESULT, (CharSequence) v);
+        } else if (v instanceof boolean[]) {
+            b.putBooleanArray(KEY_RESULT, (boolean[]) v);
+        } else if (v instanceof byte[]) {
+            b.putByteArray(KEY_RESULT, (byte[]) v);
+        } else if (v instanceof String[]) {
+            b.putStringArray(KEY_RESULT, (String[]) v);
+        } else if (v instanceof CharSequence[]) {
+            // Must be after String[] and before Object[]
+            b.putCharSequenceArray(KEY_RESULT, (CharSequence[]) v);
+        } else if (v instanceof IBinder) {
+            b.putBinder(KEY_RESULT, (IBinder) v);
+        } else if (v instanceof Parcelable[]) {
+            b.putParcelableArray(KEY_RESULT, (Parcelable[]) v);
+        } else if (v instanceof int[]) {
+            b.putIntArray(KEY_RESULT, (int[]) v);
+        } else if (v instanceof long[]) {
+            b.putLongArray(KEY_RESULT, (long[]) v);
+        } else if (v instanceof Byte) {
+            b.putByte(KEY_RESULT, (Byte) v);
+        }
+        return b;
+    }
+
+    public abstract static class Stub {
+
+        /**
+         * The name of an interface should be a fully qualified name to prevent
+         * namespace collisions. Example: "com.myproject.MyPlaybackInterface"
+         *
+         * @return The name of this interface
+         */
+        public abstract String getName();
+
+        /**
+         * This is called when a command is received that matches the interface
+         * you registered. Commands can come from any app with a MediaController
+         * reference to the session.
+         *
+         * @see MediaController
+         * @see MediaSession
+         * @param command The command or method to invoke.
+         * @param args Any args that were included with the command. May be
+         *            null.
+         * @param cb The callback provided to send a response on. May be null.
+         */
+        public abstract void onCommand(String command, Bundle args, ResultReceiver cb);
+
+        public final void sendEvent(MediaSession session, String event, Bundle extras) {
+            // TODO
+        }
+    }
+
+    /**
+     * An EventListener can be registered by an app with TODO to handle events
+     * sent by the session on a specific interface.
+     */
+    public static abstract class EventListener {
+        /**
+         * This is called when an event is received from the interface. Events
+         * are sent by the session owner and will be delivered to all
+         * controllers that are listening to the interface.
+         *
+         * @param event The event that occurred.
+         * @param args Any extras that were included with the event. May be
+         *            null.
+         */
+        public abstract void onEvent(String event, Bundle args);
+    }
+
+    private static final class EventHandler extends Handler {
+
+        private final RouteInterface.EventListener mListener;
+
+        public EventHandler(Looper looper, RouteInterface.EventListener cb) {
+            super(looper, null, true);
+            mListener = cb;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            mListener.onEvent((String) msg.obj, msg.getData());
+        }
+
+        public void postEvent(String event, Bundle args) {
+            Message msg = obtainMessage(0, event);
+            msg.setData(args);
+            msg.sendToTarget();
+        }
+    }
+}
diff --git a/media/java/android/media/session/RouteTransportControls.java b/media/java/android/media/session/RouteTransportControls.java
new file mode 100644
index 0000000..665fd10
--- /dev/null
+++ b/media/java/android/media/session/RouteTransportControls.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2014 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.session;
+
+import android.media.RemoteControlClient;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * A standard media control interface for Routes. Routes can support multiple
+ * interfaces for MediaSessions to interact with. TODO rewrite for routes
+ */
+public final class RouteTransportControls {
+    private static final String TAG = "RouteTransportControls";
+    public static final String NAME = "android.media.session.RouteTransportControls";
+
+    private static final String KEY_VALUE1 = "value1";
+
+    private static final String METHOD_FAST_FORWARD = "fastForward";
+    private static final String METHOD_GET_CURRENT_POSITION = "getCurrentPosition";
+    private static final String METHOD_GET_CAPABILITIES = "getCapabilities";
+
+    private static final String EVENT_PLAYSTATE_CHANGE = "playstateChange";
+    private static final String EVENT_METADATA_CHANGE = "metadataChange";
+
+    private final MediaController mController;
+    private final RouteInterface mIface;
+
+    private RouteTransportControls(RouteInterface iface, MediaController controller) {
+        mIface = iface;
+        mController = controller;
+    }
+
+    public static RouteTransportControls from(MediaController controller) {
+//        MediaInterface iface = controller.getInterface(NAME);
+//        if (iface != null) {
+//            return new RouteTransportControls(iface, controller);
+//        }
+        return null;
+    }
+
+    /**
+     * Send a play command to the route. TODO rename resume() and use messaging
+     * protocol, not KeyEvent
+     */
+    public void play() {
+        // TODO
+    }
+
+    /**
+     * Send a pause command to the session.
+     */
+    public void pause() {
+        // TODO
+    }
+
+    /**
+     * Set the rate at which to fastforward. Valid values are in the range [0,1]
+     * with actual rates depending on the implementation.
+     *
+     * @param rate
+     */
+    public void fastForward(float rate) {
+        if (rate < 0 || rate > 1) {
+            throw new IllegalArgumentException("Rate must be between 0 and 1 inclusive");
+        }
+        Bundle b = new Bundle();
+        b.putFloat(KEY_VALUE1, rate);
+        mIface.sendCommand(METHOD_FAST_FORWARD, b, null);
+    }
+
+    public void getCurrentPosition(ResultReceiver cb) {
+        mIface.sendCommand(METHOD_GET_CURRENT_POSITION, null, cb);
+    }
+
+    public void getCapabilities(ResultReceiver cb) {
+        mIface.sendCommand(METHOD_GET_CAPABILITIES, null, cb);
+    }
+
+    public void addListener(Listener listener) {
+        mIface.addListener(listener.mListener);
+    }
+
+    public void addListener(Listener listener, Handler handler) {
+        mIface.addListener(listener.mListener, handler);
+    }
+
+    public void removeListener(Listener listener) {
+        mIface.removeListener(listener.mListener);
+    }
+
+    public static abstract class Stub extends RouteInterface.Stub {
+        private final MediaSession mSession;
+
+        public Stub(MediaSession session) {
+            mSession = session;
+        }
+
+        @Override
+        public String getName() {
+            return NAME;
+        }
+
+        @Override
+        public void onCommand(String method, Bundle extras, ResultReceiver cb) {
+            if (TextUtils.isEmpty(method)) {
+                return;
+            }
+            Bundle result;
+            if (METHOD_FAST_FORWARD.equals(method)) {
+                fastForward(extras.getFloat(KEY_VALUE1, -1));
+            } else if (METHOD_GET_CURRENT_POSITION.equals(method)) {
+                if (cb != null) {
+                    result = new Bundle();
+                    result.putLong(KEY_VALUE1, getCurrentPosition());
+                    cb.send(0, result);
+                }
+            } else if (METHOD_GET_CAPABILITIES.equals(method)) {
+                if (cb != null) {
+                    result = new Bundle();
+                    result.putLong(KEY_VALUE1, getCapabilities());
+                    cb.send(0, result);
+                }
+            }
+        }
+
+        /**
+         * Override to handle fast forwarding. Valid values are [0,1] inclusive.
+         * The interpretation of the rate is up to the implementation. If no
+         * rate was included with the command a rate of -1 will be used by
+         * default.
+         *
+         * @param rate The rate at which to fast forward as a multiplier
+         */
+        public void fastForward(float rate) {
+            Log.w(TAG, "fastForward is not supported.");
+        }
+
+        /**
+         * Override to handle getting the current position of playback in
+         * millis.
+         *
+         * @return The current position in millis or -1
+         */
+        public long getCurrentPosition() {
+            Log.w(TAG, "getCurrentPosition is not supported");
+            return -1;
+        }
+
+        /**
+         * Override to handle getting the set of capabilities currently
+         * available.
+         *
+         * @return A bit mask of the supported capabilities
+         */
+        public long getCapabilities() {
+            Log.w(TAG, "getCapabilities is not supported");
+            return 0;
+        }
+
+        /**
+         * Publish the current playback state to the system and any controllers.
+         * Valid values are defined in {@link RemoteControlClient}. TODO move
+         * play states somewhere else.
+         *
+         * @param state
+         */
+        public final void updatePlaybackState(int state) {
+            Bundle extras = new Bundle();
+            extras.putInt(KEY_VALUE1, state);
+            sendEvent(mSession, EVENT_PLAYSTATE_CHANGE, extras);
+        }
+    }
+
+    /**
+     * Register this event listener using TODO to receive
+     * TransportControlInterface events from a session.
+     *
+     * @see RouteInterface.EventListener
+     */
+    public static abstract class Listener {
+
+        private RouteInterface.EventListener mListener = new RouteInterface.EventListener() {
+            @Override
+            public final void onEvent(String event, Bundle args) {
+                if (EVENT_PLAYSTATE_CHANGE.equals(event)) {
+                    onPlaybackStateChange(args.getInt(KEY_VALUE1));
+                } else if (EVENT_METADATA_CHANGE.equals(event)) {
+                    onMetadataUpdate(args);
+                }
+            }
+        };
+
+        /**
+         * Override to handle updates to the playback state. Valid values are in
+         * {@link TransportPerformer}. TODO put playstate values somewhere more
+         * generic.
+         *
+         * @param state
+         */
+        public void onPlaybackStateChange(int state) {
+        }
+
+        /**
+         * Override to handle metadata changes for this session's media. The
+         * default supported fields are those in {@link MediaMetadata}.
+         *
+         * @param metadata
+         */
+        public void onMetadataUpdate(Bundle metadata) {
+        }
+    }
+
+}
diff --git a/media/java/android/media/session/TransportController.java b/media/java/android/media/session/TransportController.java
new file mode 100644
index 0000000..15b11f3
--- /dev/null
+++ b/media/java/android/media/session/TransportController.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2014 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.session;
+
+import android.media.Rating;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Interface for controlling media playback on a session. This allows an app to
+ * request changes in playback, retrieve the current playback state and
+ * metadata, and listen for changes to the playback state and metadata.
+ */
+public final class TransportController {
+    private static final String TAG = "TransportController";
+
+    private final Object mLock = new Object();
+    private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>();
+    private final IMediaController mBinder;
+
+    /**
+     * @hide
+     */
+    public TransportController(IMediaController binder) {
+        mBinder = binder;
+    }
+
+    /**
+     * Start listening to changes in playback state.
+     */
+    public void addStateListener(TransportStateListener listener) {
+        addStateListener(listener, null);
+    }
+
+    public void addStateListener(TransportStateListener listener, Handler handler) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            if (getHandlerForListenerLocked(listener) != null) {
+                Log.w(TAG, "Listener is already added, ignoring");
+                return;
+            }
+            if (handler == null) {
+                handler = new Handler();
+            }
+
+            MessageHandler msgHandler = new MessageHandler(handler.getLooper(), listener);
+            mListeners.add(msgHandler);
+        }
+    }
+
+    /**
+     * Stop listening to changes in playback state.
+     */
+    public void removeStateListener(TransportStateListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            removeStateListenerLocked(listener);
+        }
+    }
+
+    /**
+     * Request that the player start its playback at its current position.
+     */
+    public void play() {
+        try {
+            mBinder.play();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling play.", e);
+        }
+    }
+
+    /**
+     * Request that the player pause its playback and stay at its current
+     * position.
+     */
+    public void pause() {
+        try {
+            mBinder.pause();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling pause.", e);
+        }
+    }
+
+    /**
+     * Request that the player stop its playback; it may clear its state in
+     * whatever way is appropriate.
+     */
+    public void stop() {
+        try {
+            mBinder.stop();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling stop.", e);
+        }
+    }
+
+    /**
+     * Move to a new location in the media stream.
+     *
+     * @param pos Position to move to, in milliseconds.
+     */
+    public void seekTo(long pos) {
+        try {
+            mBinder.seekTo(pos);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling seekTo.", e);
+        }
+    }
+
+    /**
+     * Start fast forwarding. If playback is already fast forwarding this may
+     * increase the rate.
+     */
+    public void fastForward() {
+        try {
+            mBinder.fastForward();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling fastForward.", e);
+        }
+    }
+
+    /**
+     * Skip to the next item.
+     */
+    public void next() {
+        try {
+            mBinder.next();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling next.", e);
+        }
+    }
+
+    /**
+     * Start rewinding. If playback is already rewinding this may increase the
+     * rate.
+     */
+    public void rewind() {
+        try {
+            mBinder.rewind();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling rewind.", e);
+        }
+    }
+
+    /**
+     * Skip to the previous item.
+     */
+    public void previous() {
+        try {
+            mBinder.previous();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling previous.", e);
+        }
+    }
+
+    /**
+     * Rate the current content. This will cause the rating to be set for the
+     * current user. The Rating type must match the type returned by
+     * {@link #getRatingType()}.
+     *
+     * @param rating The rating to set for the current content
+     */
+    public void rate(Rating rating) {
+        try {
+            mBinder.rate(rating);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling rate.", e);
+        }
+    }
+
+    /**
+     * Get the rating type supported by the session. One of:
+     * <ul>
+     * <li>{@link Rating#RATING_NONE}</li>
+     * <li>{@link Rating#RATING_HEART}</li>
+     * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
+     * <li>{@link Rating#RATING_3_STARS}</li>
+     * <li>{@link Rating#RATING_4_STARS}</li>
+     * <li>{@link Rating#RATING_5_STARS}</li>
+     * <li>{@link Rating#RATING_PERCENTAGE}</li>
+     * </ul>
+     *
+     * @return The supported rating type
+     */
+    public int getRatingType() {
+        try {
+            return mBinder.getRatingType();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling getRatingType.", e);
+            return Rating.RATING_NONE;
+        }
+    }
+
+    /**
+     * Get the current playback state for this session.
+     *
+     * @return The current PlaybackState or null
+     */
+    public PlaybackState getPlaybackState() {
+        try {
+            return mBinder.getPlaybackState();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling getPlaybackState.", e);
+            return null;
+        }
+    }
+
+    /**
+     * Get the current metadata for this session.
+     *
+     * @return The current MediaMetadata or null.
+     */
+    public MediaMetadata getMetadata() {
+        try {
+            return mBinder.getMetadata();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling getMetadata.", e);
+            return null;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public final void postPlaybackStateChanged(PlaybackState state) {
+        synchronized (mLock) {
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                mListeners.get(i).post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state);
+            }
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public final void postMetadataChanged(MediaMetadata metadata) {
+        synchronized (mLock) {
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                mListeners.get(i).post(MessageHandler.MSG_UPDATE_METADATA,
+                        metadata);
+            }
+        }
+    }
+
+    private MessageHandler getHandlerForListenerLocked(TransportStateListener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mListeners.get(i);
+            if (listener == handler.mListener) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private boolean removeStateListenerLocked(TransportStateListener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            if (listener == mListeners.get(i).mListener) {
+                mListeners.remove(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Register using {@link #addStateListener} to receive updates when there
+     * are playback changes on the session.
+     */
+    public static abstract class TransportStateListener {
+        private MessageHandler mHandler;
+        /**
+         * Override to handle changes in playback state.
+         *
+         * @param state The new playback state of the session
+         */
+        public void onPlaybackStateChanged(PlaybackState state) {
+        }
+
+        /**
+         * Override to handle changes to the current metadata.
+         *
+         * @see MediaMetadata
+         * @param metadata The current metadata for the session or null
+         */
+        public void onMetadataChanged(MediaMetadata metadata) {
+        }
+
+        private void setHandler(Handler handler) {
+            mHandler = new MessageHandler(handler.getLooper(), this);
+        }
+    }
+
+    private static class MessageHandler extends Handler {
+        private static final int MSG_UPDATE_PLAYBACK_STATE = 1;
+        private static final int MSG_UPDATE_METADATA = 2;
+
+        private TransportStateListener mListener;
+
+        public MessageHandler(Looper looper, TransportStateListener cb) {
+            super(looper, null, true);
+            mListener = cb;
+        }
+
+        public void post(int msg, Object obj) {
+            obtainMessage(msg, obj).sendToTarget();
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_UPDATE_PLAYBACK_STATE:
+                    mListener.onPlaybackStateChanged((PlaybackState) msg.obj);
+                    break;
+                case MSG_UPDATE_METADATA:
+                    mListener.onMetadataChanged((MediaMetadata) msg.obj);
+                    break;
+            }
+        }
+    }
+
+}
diff --git a/media/java/android/media/session/TransportPerformer.java b/media/java/android/media/session/TransportPerformer.java
new file mode 100644
index 0000000..b96db20
--- /dev/null
+++ b/media/java/android/media/session/TransportPerformer.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2014 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.session;
+
+import android.media.AudioManager;
+import android.media.Rating;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Allows broadcasting of playback changes.
+ */
+public final class TransportPerformer {
+    private static final String TAG = "TransportPerformer";
+    private final Object mLock = new Object();
+    private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>();
+
+    private IMediaSession mBinder;
+
+    /**
+     * @hide
+     */
+    public TransportPerformer(IMediaSession binder) {
+        mBinder = binder;
+    }
+
+    /**
+     * Add a listener to receive updates on.
+     *
+     * @param listener The callback object
+     */
+    public void addListener(Listener listener) {
+        addListener(listener, null);
+    }
+
+    /**
+     * Add a listener to receive updates on. The updates will be posted to the
+     * specified handler. If no handler is provided they will be posted to the
+     * caller's thread.
+     *
+     * @param listener The listener to receive updates on
+     * @param handler The handler to post the updates on
+     */
+    public void addListener(Listener listener, Handler handler) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            if (getHandlerForListenerLocked(listener) != null) {
+                Log.w(TAG, "Listener is already added, ignoring");
+            }
+            if (handler == null) {
+                handler = new Handler();
+            }
+            MessageHandler msgHandler = new MessageHandler(handler.getLooper(), listener);
+            mListeners.add(msgHandler);
+        }
+    }
+
+    /**
+     * Stop receiving updates on the specified handler. If an update has already
+     * been posted you may still receive it after this call returns.
+     *
+     * @param listener The listener to stop receiving updates on
+     */
+    public void removeListener(Listener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            removeListenerLocked(listener);
+        }
+    }
+
+    /**
+     * Update the current playback state.
+     *
+     * @param state The current state of playback
+     */
+    public final void setPlaybackState(PlaybackState state) {
+        try {
+            mBinder.setPlaybackState(state);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
+        }
+    }
+
+    /**
+     * Update the current metadata. New metadata can be created using
+     * {@link MediaMetadata.Builder}.
+     *
+     * @param metadata The new metadata
+     */
+    public final void setMetadata(MediaMetadata metadata) {
+        try {
+            mBinder.setMetadata(metadata);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public final void onPlay() {
+        post(MessageHandler.MESSAGE_PLAY);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onPause() {
+        post(MessageHandler.MESSAGE_PAUSE);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onStop() {
+        post(MessageHandler.MESSAGE_STOP);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onNext() {
+        post(MessageHandler.MESSAGE_NEXT);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onPrevious() {
+        post(MessageHandler.MESSAGE_PREVIOUS);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onFastForward() {
+        post(MessageHandler.MESSAGE_FAST_FORWARD);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onRewind() {
+        post(MessageHandler.MESSAGE_REWIND);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onSeekTo(long pos) {
+        post(MessageHandler.MESSAGE_SEEK_TO, pos);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onRate(Rating rating) {
+        post(MessageHandler.MESSAGE_RATE, rating);
+    }
+
+    private MessageHandler getHandlerForListenerLocked(Listener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mListeners.get(i);
+            if (listener == handler.mListener) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private boolean removeListenerLocked(Listener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            if (listener == mListeners.get(i).mListener) {
+                mListeners.remove(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void post(int what, Object obj) {
+        synchronized (mLock) {
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                mListeners.get(i).post(what, obj);
+            }
+        }
+    }
+
+    private void post(int what) {
+        post(what, null);
+    }
+
+    /**
+     * Extend Listener to handle transport controls. Listeners can be registered
+     * using {@link #addListener}.
+     */
+    public static abstract class Listener {
+
+        /**
+         * Override to handle requests to begin playback.
+         */
+        public void onPlay() {
+        }
+
+        /**
+         * Override to handle requests to pause playback.
+         */
+        public void onPause() {
+        }
+
+        /**
+         * Override to handle requests to skip to the next media item.
+         */
+        public void onNext() {
+        }
+
+        /**
+         * Override to handle requests to skip to the previous media item.
+         */
+        public void onPrevious() {
+        }
+
+        /**
+         * Override to handle requests to fast forward.
+         */
+        public void onFastForward() {
+        }
+
+        /**
+         * Override to handle requests to rewind.
+         */
+        public void onRewind() {
+        }
+
+        /**
+         * Override to handle requests to stop playback.
+         */
+        public void onStop() {
+        }
+
+        /**
+         * Override to handle requests to seek to a specific position in ms.
+         *
+         * @param pos New position to move to, in milliseconds.
+         */
+        public void onSeekTo(long pos) {
+        }
+
+        /**
+         * Override to handle the item being rated.
+         *
+         * @param rating
+         */
+        public void onRate(Rating rating) {
+        }
+
+        /**
+         * Report that audio focus has changed on the app. This only happens if
+         * you have indicated you have started playing with
+         * {@link #setPlaybackState}. TODO figure out route focus apis/handling.
+         *
+         * @param focusChange The type of focus change, TBD. The default
+         *            implementation will deliver a call to {@link #onPause}
+         *            when focus is lost.
+         */
+        public void onRouteFocusChange(int focusChange) {
+            switch (focusChange) {
+                case AudioManager.AUDIOFOCUS_LOSS:
+                    onPause();
+                    break;
+            }
+        }
+    }
+
+    private class MessageHandler extends Handler {
+        private static final int MESSAGE_PLAY = 1;
+        private static final int MESSAGE_PAUSE = 2;
+        private static final int MESSAGE_STOP = 3;
+        private static final int MESSAGE_NEXT = 4;
+        private static final int MESSAGE_PREVIOUS = 5;
+        private static final int MESSAGE_FAST_FORWARD = 6;
+        private static final int MESSAGE_REWIND = 7;
+        private static final int MESSAGE_SEEK_TO = 8;
+        private static final int MESSAGE_RATE = 9;
+
+        private Listener mListener;
+
+        public MessageHandler(Looper looper, Listener cb) {
+            super(looper);
+            mListener = cb;
+        }
+
+        public void post(int what, Object obj) {
+            obtainMessage(what, obj).sendToTarget();
+        }
+
+        public void post(int what) {
+            post(what, null);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MESSAGE_PLAY:
+                    mListener.onPlay();
+                    break;
+                case MESSAGE_PAUSE:
+                    mListener.onPause();
+                    break;
+                case MESSAGE_STOP:
+                    mListener.onStop();
+                    break;
+                case MESSAGE_NEXT:
+                    mListener.onNext();
+                    break;
+                case MESSAGE_PREVIOUS:
+                    mListener.onPrevious();
+                    break;
+                case MESSAGE_FAST_FORWARD:
+                    mListener.onFastForward();
+                    break;
+                case MESSAGE_REWIND:
+                    mListener.onRewind();
+                    break;
+                case MESSAGE_SEEK_TO:
+                    mListener.onSeekTo((Long) msg.obj);
+                    break;
+                case MESSAGE_RATE:
+                    mListener.onRate((Rating) msg.obj);
+                    break;
+            }
+        }
+    }
+}
diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java
index cc464db..2ddbb7d 100755
--- a/media/java/android/mtp/MtpDatabase.java
+++ b/media/java/android/mtp/MtpDatabase.java
@@ -16,15 +16,19 @@
 
 package android.mtp;
 
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.ContentValues;
 import android.content.IContentProvider;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.media.MediaScanner;
 import android.net.Uri;
+import android.os.BatteryManager;
+import android.os.BatteryStats;
 import android.os.RemoteException;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio;
@@ -113,11 +117,35 @@
                                             + Files.FileColumns.PARENT + "=?";
 
     private final MediaScanner mMediaScanner;
+    private MtpServer mServer;
+
+    // read from native code
+    private int mBatteryLevel;
+    private int mBatteryScale;
 
     static {
         System.loadLibrary("media_jni");
     }
 
+    private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
+          @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
+                mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
+                int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+                if (newLevel != mBatteryLevel) {
+                    mBatteryLevel = newLevel;
+                    if (mServer != null) {
+                        // send device property changed event
+                        mServer.sendDevicePropertyChanged(
+                                MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
+                    }
+                }
+            }
+        }
+    };
+
     public MtpDatabase(Context context, String volumeName, String storagePath,
             String[] subDirectories) {
         native_setup();
@@ -171,6 +199,18 @@
         initDeviceProperties(context);
     }
 
+    public void setServer(MtpServer server) {
+        mServer = server;
+
+        // register for battery notifications when we are connected
+        if (server != null) {
+            mContext.registerReceiver(mBatteryReceiver,
+                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+        } else {
+            mContext.unregisterReceiver(mBatteryReceiver);
+        }
+    }
+
     @Override
     protected void finalize() throws Throwable {
         try {
@@ -663,6 +703,7 @@
             MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
             MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
             MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
+            MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
         };
     }
 
@@ -819,6 +860,8 @@
                 outStringValue[imageSize.length()] = 0;
                 return MtpConstants.RESPONSE_OK;
 
+            // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code
+
             default:
                 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
         }
diff --git a/media/java/android/mtp/MtpServer.java b/media/java/android/mtp/MtpServer.java
index 266f78e..3814630 100644
--- a/media/java/android/mtp/MtpServer.java
+++ b/media/java/android/mtp/MtpServer.java
@@ -30,6 +30,7 @@
 
     public MtpServer(MtpDatabase database, boolean usePtp) {
         native_setup(database, usePtp);
+        database.setServer(this);
     }
 
     public void start() {
@@ -51,6 +52,10 @@
         native_send_object_removed(handle);
     }
 
+    public void sendDevicePropertyChanged(int property) {
+        native_send_device_property_changed(property);
+    }
+
     public void addStorage(MtpStorage storage) {
         native_add_storage(storage);
     }
@@ -64,6 +69,7 @@
     private native final void native_cleanup();
     private native final void native_send_object_added(int handle);
     private native final void native_send_object_removed(int handle);
+    private native final void native_send_device_property_changed(int property);
     private native final void native_add_storage(MtpStorage storage);
     private native final void native_remove_storage(int storageId);
 }
diff --git a/media/jni/android_mtp_MtpDatabase.cpp b/media/jni/android_mtp_MtpDatabase.cpp
index 6b0bd0d..ea75a18 100644
--- a/media/jni/android_mtp_MtpDatabase.cpp
+++ b/media/jni/android_mtp_MtpDatabase.cpp
@@ -68,6 +68,8 @@
 static jmethodID method_sessionEnded;
 
 static jfieldID field_context;
+static jfieldID field_batteryLevel;
+static jfieldID field_batteryScale;
 
 // MtpPropertyList fields
 static jfieldID field_mCount;
@@ -527,68 +529,75 @@
 
 MtpResponseCode MyMtpDatabase::getDevicePropertyValue(MtpDeviceProperty property,
                                             MtpDataPacket& packet) {
-    int         type;
-
-    if (!getDevicePropertyInfo(property, type))
-        return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
-
     JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jint result = env->CallIntMethod(mDatabase, method_getDeviceProperty,
-                (jint)property, mLongBuffer, mStringBuffer);
-    if (result != MTP_RESPONSE_OK) {
+
+    if (property == MTP_DEVICE_PROPERTY_BATTERY_LEVEL) {
+        // special case - implemented here instead of Java
+        packet.putUInt8((uint8_t)env->GetIntField(mDatabase, field_batteryLevel));
+        return MTP_RESPONSE_OK;
+    } else {
+        int type;
+
+        if (!getDevicePropertyInfo(property, type))
+            return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
+
+        jint result = env->CallIntMethod(mDatabase, method_getDeviceProperty,
+                    (jint)property, mLongBuffer, mStringBuffer);
+        if (result != MTP_RESPONSE_OK) {
+            checkAndClearExceptionFromCallback(env, __FUNCTION__);
+            return result;
+        }
+
+        jlong* longValues = env->GetLongArrayElements(mLongBuffer, 0);
+        jlong longValue = longValues[0];
+        env->ReleaseLongArrayElements(mLongBuffer, longValues, 0);
+
+        switch (type) {
+            case MTP_TYPE_INT8:
+                packet.putInt8(longValue);
+                break;
+            case MTP_TYPE_UINT8:
+                packet.putUInt8(longValue);
+                break;
+            case MTP_TYPE_INT16:
+                packet.putInt16(longValue);
+                break;
+            case MTP_TYPE_UINT16:
+                packet.putUInt16(longValue);
+                break;
+            case MTP_TYPE_INT32:
+                packet.putInt32(longValue);
+                break;
+            case MTP_TYPE_UINT32:
+                packet.putUInt32(longValue);
+                break;
+            case MTP_TYPE_INT64:
+                packet.putInt64(longValue);
+                break;
+            case MTP_TYPE_UINT64:
+                packet.putUInt64(longValue);
+                break;
+            case MTP_TYPE_INT128:
+                packet.putInt128(longValue);
+                break;
+            case MTP_TYPE_UINT128:
+                packet.putInt128(longValue);
+                break;
+            case MTP_TYPE_STR:
+            {
+                jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
+                packet.putString(str);
+                env->ReleaseCharArrayElements(mStringBuffer, str, 0);
+                break;
+             }
+            default:
+                ALOGE("unsupported type in getDevicePropertyValue\n");
+                return MTP_RESPONSE_INVALID_DEVICE_PROP_FORMAT;
+        }
+
         checkAndClearExceptionFromCallback(env, __FUNCTION__);
-        return result;
+        return MTP_RESPONSE_OK;
     }
-
-    jlong* longValues = env->GetLongArrayElements(mLongBuffer, 0);
-    jlong longValue = longValues[0];
-    env->ReleaseLongArrayElements(mLongBuffer, longValues, 0);
-
-    switch (type) {
-        case MTP_TYPE_INT8:
-            packet.putInt8(longValue);
-            break;
-        case MTP_TYPE_UINT8:
-            packet.putUInt8(longValue);
-            break;
-        case MTP_TYPE_INT16:
-            packet.putInt16(longValue);
-            break;
-        case MTP_TYPE_UINT16:
-            packet.putUInt16(longValue);
-            break;
-        case MTP_TYPE_INT32:
-            packet.putInt32(longValue);
-            break;
-        case MTP_TYPE_UINT32:
-            packet.putUInt32(longValue);
-            break;
-        case MTP_TYPE_INT64:
-            packet.putInt64(longValue);
-            break;
-        case MTP_TYPE_UINT64:
-            packet.putUInt64(longValue);
-            break;
-        case MTP_TYPE_INT128:
-            packet.putInt128(longValue);
-            break;
-        case MTP_TYPE_UINT128:
-            packet.putInt128(longValue);
-            break;
-        case MTP_TYPE_STR:
-        {
-            jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
-            packet.putString(str);
-            env->ReleaseCharArrayElements(mStringBuffer, str, 0);
-            break;
-         }
-        default:
-            ALOGE("unsupported type in getDevicePropertyValue\n");
-            return MTP_RESPONSE_INVALID_DEVICE_PROP_FORMAT;
-    }
-
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-    return MTP_RESPONSE_OK;
 }
 
 MtpResponseCode MyMtpDatabase::setDevicePropertyValue(MtpDeviceProperty property,
@@ -923,6 +932,7 @@
     {   MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,    MTP_TYPE_STR },
     {   MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,       MTP_TYPE_STR },
     {   MTP_DEVICE_PROPERTY_IMAGE_SIZE,                 MTP_TYPE_STR },
+    {   MTP_DEVICE_PROPERTY_BATTERY_LEVEL,              MTP_TYPE_UINT8 },
 };
 
 bool MyMtpDatabase::getObjectPropertyInfo(MtpObjectProperty property, int& type) {
@@ -1046,7 +1056,7 @@
         case MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
             writable = true;
             // fall through
-        case MTP_DEVICE_PROPERTY_IMAGE_SIZE:
+        case MTP_DEVICE_PROPERTY_IMAGE_SIZE: {
             result = new MtpProperty(property, MTP_TYPE_STR, writable);
 
             // get current value
@@ -1063,6 +1073,12 @@
                 ALOGE("unable to read device property, response: %04X", ret);
             }
             break;
+        }
+        case MTP_DEVICE_PROPERTY_BATTERY_LEVEL:
+            result = new MtpProperty(property, MTP_TYPE_UINT8);
+            result->setFormRange(0, env->GetIntField(mDatabase, field_batteryScale), 1);
+            result->mCurrentValue.u.u8 = (uint8_t)env->GetIntField(mDatabase, field_batteryLevel);
+            break;
     }
 
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
@@ -1234,6 +1250,16 @@
         ALOGE("Can't find MtpDatabase.mNativeContext");
         return -1;
     }
+    field_batteryLevel = env->GetFieldID(clazz, "mBatteryLevel", "I");
+    if (field_batteryLevel == NULL) {
+        ALOGE("Can't find MtpDatabase.mBatteryLevel");
+        return -1;
+    }
+    field_batteryScale = env->GetFieldID(clazz, "mBatteryScale", "I");
+    if (field_batteryScale == NULL) {
+        ALOGE("Can't find MtpDatabase.mBatteryScale");
+        return -1;
+    }
 
     // now set up fields for MtpPropertyList class
     clazz = env->FindClass("android/mtp/MtpPropertyList");
diff --git a/media/jni/android_mtp_MtpServer.cpp b/media/jni/android_mtp_MtpServer.cpp
index 9d7f1c2..2f90dfe 100644
--- a/media/jni/android_mtp_MtpServer.cpp
+++ b/media/jni/android_mtp_MtpServer.cpp
@@ -118,6 +118,18 @@
 }
 
 static void
+android_mtp_MtpServer_send_device_property_changed(JNIEnv *env, jobject thiz, jint property)
+{
+    Mutex::Autolock autoLock(sMutex);
+
+    MtpServer* server = getMtpServer(env, thiz);
+    if (server)
+        server->sendDevicePropertyChanged(property);
+    else
+        ALOGE("server is null in send_object_removed");
+}
+
+static void
 android_mtp_MtpServer_add_storage(JNIEnv *env, jobject thiz, jobject jstorage)
 {
     Mutex::Autolock autoLock(sMutex);
@@ -174,6 +186,8 @@
     {"native_cleanup",              "()V",  (void *)android_mtp_MtpServer_cleanup},
     {"native_send_object_added",    "(I)V", (void *)android_mtp_MtpServer_send_object_added},
     {"native_send_object_removed",  "(I)V", (void *)android_mtp_MtpServer_send_object_removed},
+    {"native_send_device_property_changed",  "(I)V",
+                                    (void *)android_mtp_MtpServer_send_device_property_changed},
     {"native_add_storage",          "(Landroid/mtp/MtpStorage;)V",
                                             (void *)android_mtp_MtpServer_add_storage},
     {"native_remove_storage",       "(I)V", (void *)android_mtp_MtpServer_remove_storage},
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 56c1f4e..1693e01 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -58,12 +58,6 @@
             android:layout_height="@dimen/notification_panel_header_height"
             />
 
-        <com.android.systemui.statusbar.phone.ZenModeView
-            android:id="@+id/zenmode"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            />
-
         <TextView
             android:id="@+id/emergency_calls_only"
             android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Network.EmergencyOnly"
diff --git a/packages/SystemUI/res/layout/status_bar_expanded_header.xml b/packages/SystemUI/res/layout/status_bar_expanded_header.xml
index 25c516b..9aa7cfd 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded_header.xml
@@ -15,7 +15,7 @@
 ** limitations under the License.
 -->
 
-<com.android.systemui.statusbar.phone.PanelHeaderView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:systemui="http://schemas.android.com/apk/res/com.android.systemui"
     android:id="@+id/header"
@@ -106,4 +106,4 @@
             android:contentDescription="@string/accessibility_notifications_button"
             />
     </FrameLayout>
-</com.android.systemui.statusbar.phone.PanelHeaderView>
+</LinearLayout>
diff --git a/packages/SystemUI/res/values-af/strings.xml b/packages/SystemUI/res/values-af/strings.xml
index 31428b4..98d132a 100644
--- a/packages/SystemUI/res/values-af/strings.xml
+++ b/packages/SystemUI/res/values-af/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Kleur-omkeringmodus"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Verbeterde kontrasmodus"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Kleurregstellingmodus"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"ONLANGS"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Netwerk word\ndalk gemonitor"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Soek"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Gly op vir <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-am/strings.xml b/packages/SystemUI/res/values-am/strings.xml
index e393c90..d7457f0 100644
--- a/packages/SystemUI/res/values-am/strings.xml
+++ b/packages/SystemUI/res/values-am/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"የተቃራኒ ቀለም ሁነታ"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"የተሻሻለ ንፅፅር ሁነታ"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"የቀለም እርማት ሁነታ"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"የቅርብ ጊዜዎች"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"አውታረ መረብ\nክትትል ሊደረግበት ይችላል"</string>
     <string name="description_target_search" msgid="3091587249776033139">"ፍለጋ"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"ለ<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> ወደ ላይ አንሸራትት።"</string>
diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml
index 0060004..d693e91 100644
--- a/packages/SystemUI/res/values-ar/strings.xml
+++ b/packages/SystemUI/res/values-ar/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"وضع انعكاس اللون"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"وضع التباين المحسن"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"وضع تصحيح الألوان"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"قد تكون الشبكة\nخاضعة للرقابة"</string>
     <string name="description_target_search" msgid="3091587249776033139">"بحث"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"تمرير لأعلى لـ <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-bg/strings.xml b/packages/SystemUI/res/values-bg/strings.xml
index 1488864..dc53861 100644
--- a/packages/SystemUI/res/values-bg/strings.xml
+++ b/packages/SystemUI/res/values-bg/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Режим на инвертиране на цветовете"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Режим на подобрен контраст"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Режим на коригиране на цветовете"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Мрежата може\nда се наблюдава"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Търсене"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Плъзнете нагоре за <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-ca/strings.xml b/packages/SystemUI/res/values-ca/strings.xml
index 329e91f..e79e094 100644
--- a/packages/SystemUI/res/values-ca/strings.xml
+++ b/packages/SystemUI/res/values-ca/strings.xml
@@ -206,6 +206,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Mode d\'inversió de color"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Mode de contrast millorat"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Mode de correcció de color"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"RECENTS"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"És possible que la xarxa\nestigui controlada"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Cerca"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Fes lliscar el dit cap amunt per <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml
index b326220..60bbe3b 100644
--- a/packages/SystemUI/res/values-cs/strings.xml
+++ b/packages/SystemUI/res/values-cs/strings.xml
@@ -206,6 +206,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Režim převrácení barev"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Režim zvýšeného kontrastu"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Režim korekce barev"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"POSLEDNÍ"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Síť může být\nmonitorována"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Vyhledávání"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Přejeďte prstem nahoru: <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>"</string>
diff --git a/packages/SystemUI/res/values-da/strings.xml b/packages/SystemUI/res/values-da/strings.xml
index f0db449..5b9f633 100644
--- a/packages/SystemUI/res/values-da/strings.xml
+++ b/packages/SystemUI/res/values-da/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Farveinverteringstilstand"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Tilstand for forbedret kontrast"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Farvekorrigeringstilstand"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Netværket kan\nvære overvåget"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Søgning"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Glid op for at <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-de/strings.xml b/packages/SystemUI/res/values-de/strings.xml
index fd41752..c8cb0a4 100644
--- a/packages/SystemUI/res/values-de/strings.xml
+++ b/packages/SystemUI/res/values-de/strings.xml
@@ -206,6 +206,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Farbinversionsmodus"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Kontrastverbesserungsmodus"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Farbkorrekturmodus"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"Letzte"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Netzwerk wird\neventuell überwacht."</string>
     <string name="description_target_search" msgid="3091587249776033139">"Suche"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Zum <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> nach oben schieben"</string>
diff --git a/packages/SystemUI/res/values-el/strings.xml b/packages/SystemUI/res/values-el/strings.xml
index 96e2aaa..79025b6 100644
--- a/packages/SystemUI/res/values-el/strings.xml
+++ b/packages/SystemUI/res/values-el/strings.xml
@@ -206,6 +206,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Λειτουργία αναστροφής χρώματος"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Λειτουργία βελτίωσης αντίθεσης"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Λειτουργία διόρθωσης χρώματος"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"ΠΡΟΣΦΑΤΑ"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Το δίκτυο μπορεί\nνα παρακολουθείται"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Αναζήτηση"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Κύλιση προς τα επάνω για <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml
index 342061e..338c8ac 100644
--- a/packages/SystemUI/res/values-en-rGB/strings.xml
+++ b/packages/SystemUI/res/values-en-rGB/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Colour inversion mode"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Enhanced contrast mode"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Colour correction mode"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"RECENTS"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Network may\nbe monitored"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Search"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Slide up for <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-en-rIN/strings.xml b/packages/SystemUI/res/values-en-rIN/strings.xml
index 342061e..338c8ac 100644
--- a/packages/SystemUI/res/values-en-rIN/strings.xml
+++ b/packages/SystemUI/res/values-en-rIN/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Colour inversion mode"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Enhanced contrast mode"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Colour correction mode"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"RECENTS"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Network may\nbe monitored"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Search"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Slide up for <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-es-rUS/strings.xml b/packages/SystemUI/res/values-es-rUS/strings.xml
index a82e2d7..89fe58b 100644
--- a/packages/SystemUI/res/values-es-rUS/strings.xml
+++ b/packages/SystemUI/res/values-es-rUS/strings.xml
@@ -206,6 +206,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Modo de inversión de color"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Modo de contraste mejorado"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Modo de corrección de color"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"RECIENTES"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Es posible que la red\nesté supervisada."</string>
     <string name="description_target_search" msgid="3091587249776033139">"Buscar"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Desliza el dedo hacia arriba para <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml
index 2a977ee..4999f71 100644
--- a/packages/SystemUI/res/values-es/strings.xml
+++ b/packages/SystemUI/res/values-es/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Modo de inversión de color"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Modo de contraste mejorado"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Modo de corrección de color"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"RECIENTES"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"La red se\npuede supervisar"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Buscar"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Desliza el dedo hacia arriba para <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-et-rEE/strings.xml b/packages/SystemUI/res/values-et-rEE/strings.xml
index 821c1dd..b734fa8 100644
--- a/packages/SystemUI/res/values-et-rEE/strings.xml
+++ b/packages/SystemUI/res/values-et-rEE/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Värvide ümberpööramise režiim"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Täiustatud kontrasti režiim"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Värviparandusrežiim"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"HILJUTISED"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Võrku võidakse\njälgida"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Otsing"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Lohistage üles: <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-fa/strings.xml b/packages/SystemUI/res/values-fa/strings.xml
index 2cb40a0..46caf79 100644
--- a/packages/SystemUI/res/values-fa/strings.xml
+++ b/packages/SystemUI/res/values-fa/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"حالت وارونگی رنگ"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"حالت کنتراست بهبودیافته"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"حالت تصحیح رنگ"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"ممکن است شبکه\nتحت نظارت باشد"</string>
     <string name="description_target_search" msgid="3091587249776033139">"جستجو"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"لغزاندن به بالا برای <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-fi/strings.xml b/packages/SystemUI/res/values-fi/strings.xml
index 41a0bd4..6567318 100644
--- a/packages/SystemUI/res/values-fi/strings.xml
+++ b/packages/SystemUI/res/values-fi/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Käänteinen väritila"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Kontrastinparannustila"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Värinkorjaustila"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Verkkoa saatetaan\nvalvoa"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Haku"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Liu\'uta ylös ja <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-fr-rCA/strings.xml b/packages/SystemUI/res/values-fr-rCA/strings.xml
index 60823b5..273fd9f 100644
--- a/packages/SystemUI/res/values-fr-rCA/strings.xml
+++ b/packages/SystemUI/res/values-fr-rCA/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Mode d\'inversion des couleurs"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Mode d\'accentuation du contraste"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Mode de correction des couleurs"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Le réseau peut\nêtre surveillé."</string>
     <string name="description_target_search" msgid="3091587249776033139">"Recherche"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Faire glisser le doigt vers le haut : <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>"</string>
diff --git a/packages/SystemUI/res/values-fr/strings.xml b/packages/SystemUI/res/values-fr/strings.xml
index 5d6368e..0b0da12 100644
--- a/packages/SystemUI/res/values-fr/strings.xml
+++ b/packages/SystemUI/res/values-fr/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Mode d\'inversion des couleurs"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Mode d\'accentuation du contraste"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Mode de correction des couleurs"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Le réseau peut\nêtre surveillé."</string>
     <string name="description_target_search" msgid="3091587249776033139">"Rechercher"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Faites glisser vers le haut pour <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml
index 3167fe7..9be9550 100644
--- a/packages/SystemUI/res/values-hi/strings.xml
+++ b/packages/SystemUI/res/values-hi/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"रंग व्युत्क्रम मोड"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"उन्नत कंट्रास्ट मोड"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"रंग सुधार मोड"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"हाल ही का"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"नेटवर्क को\nमॉनीटर किया जा सकता है"</string>
     <string name="description_target_search" msgid="3091587249776033139">"खोजें"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> के लिए ऊपर स्‍लाइड करें."</string>
diff --git a/packages/SystemUI/res/values-hr/strings.xml b/packages/SystemUI/res/values-hr/strings.xml
index 529f891..21d6ef5 100644
--- a/packages/SystemUI/res/values-hr/strings.xml
+++ b/packages/SystemUI/res/values-hr/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Način inverzije boje"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Način pojačanog kontrasta"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Način korekcije boje"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"NEDAVNO"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Mreža se\nmožda prati"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Pretraživanje"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Kliznite prema gore za <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-hu/strings.xml b/packages/SystemUI/res/values-hu/strings.xml
index 7e064dd..8e6ee4f 100644
--- a/packages/SystemUI/res/values-hu/strings.xml
+++ b/packages/SystemUI/res/values-hu/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Színinvertálás mód"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Kontrasztjavítás mód"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Színjavítás mód"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"LEGUTÓBBIAK"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Lehet, hogy a\nhálózat felügyelt"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Keresés"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"A(z) <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> művelethez csúsztassa felfelé."</string>
diff --git a/packages/SystemUI/res/values-hy-rAM/strings.xml b/packages/SystemUI/res/values-hy-rAM/strings.xml
index 1124163..af14c76 100644
--- a/packages/SystemUI/res/values-hy-rAM/strings.xml
+++ b/packages/SystemUI/res/values-hy-rAM/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Գունաշրջման ռեժիմ"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Ընդլայնված ցայտունության ռեժիմ"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Գույների կարգավորման ռեժիմ"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Ցանցը կարող է\nվերահսկվել"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Որոնել"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Սահեցրեք վերև <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>-ի համար:"</string>
diff --git a/packages/SystemUI/res/values-in/strings.xml b/packages/SystemUI/res/values-in/strings.xml
index 167b101..13fc534 100644
--- a/packages/SystemUI/res/values-in/strings.xml
+++ b/packages/SystemUI/res/values-in/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Mode inversi warna"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Mode kontras yang disempurnakan"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Mode koreksi warna"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"TERBARU"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Jaringan bisa\ndiawasi"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Telusuri"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Geser ke atas untuk <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-it/strings.xml b/packages/SystemUI/res/values-it/strings.xml
index 6321870..71a644d 100644
--- a/packages/SystemUI/res/values-it/strings.xml
+++ b/packages/SystemUI/res/values-it/strings.xml
@@ -206,6 +206,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Modalità inversione colori"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Modalità di contrasto avanzata"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Modalità di correzione del colore"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"ELEMENTI RECENTI"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"La rete potrebbe\nessere monitorata"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Ricerca"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Su per <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-iw/strings.xml b/packages/SystemUI/res/values-iw/strings.xml
index 24114f7..798e8c3 100644
--- a/packages/SystemUI/res/values-iw/strings.xml
+++ b/packages/SystemUI/res/values-iw/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"מצב היפוך צבעים"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"מצב ניגודיות מוגברת"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"מצב תיקון צבע"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"אחרונים"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"ייתכן שהרשת\nמנוטרת"</string>
     <string name="description_target_search" msgid="3091587249776033139">"חיפוש"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"הסט למעלה כדי להציג <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-ja/strings.xml b/packages/SystemUI/res/values-ja/strings.xml
index 0bee9f0..b0d15cf 100644
--- a/packages/SystemUI/res/values-ja/strings.xml
+++ b/packages/SystemUI/res/values-ja/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"色反転モード"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"拡張コントラストモード"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"色補正モード"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"ネットワークが監視される\n場合があります"</string>
     <string name="description_target_search" msgid="3091587249776033139">"検索します"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"上にスライドして<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>を行います。"</string>
diff --git a/packages/SystemUI/res/values-ka-rGE/strings.xml b/packages/SystemUI/res/values-ka-rGE/strings.xml
index fe0a047..a4d459f 100644
--- a/packages/SystemUI/res/values-ka-rGE/strings.xml
+++ b/packages/SystemUI/res/values-ka-rGE/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"ფერთა ინვერსიის რეჟიმი"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"გაუმჯობესებული კონტრასტის რეჟიმი"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"ფერთა კორექციის რეჟიმი"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"შესაძლოა ქსელზე\nმონიტორინგი ხორციელდებოდეს"</string>
     <string name="description_target_search" msgid="3091587249776033139">"ძიება"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"გაასრიალეთ ზემოთ <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>-თვის."</string>
diff --git a/packages/SystemUI/res/values-km-rKH/strings.xml b/packages/SystemUI/res/values-km-rKH/strings.xml
index 470309a..d0d1c120 100644
--- a/packages/SystemUI/res/values-km-rKH/strings.xml
+++ b/packages/SystemUI/res/values-km-rKH/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"របៀប​​បញ្ច្រាស​ពណ៌"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"របៀប​កម្រិត​ពណ៌​ប្រ​សើ​រ​ឡើង"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"របៀប​កែ​ពណ៌"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"បណ្ដាញ​អាច​\nត្រូវ​បាន​ត្រួតពិនិត្យ"</string>
     <string name="description_target_search" msgid="3091587249776033139">"ស្វែងរក"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"រុញ​ឡើង​លើ​ដើម្បី <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> ។"</string>
diff --git a/packages/SystemUI/res/values-ko/strings.xml b/packages/SystemUI/res/values-ko/strings.xml
index f72549d..bda4a95 100644
--- a/packages/SystemUI/res/values-ko/strings.xml
+++ b/packages/SystemUI/res/values-ko/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"색상 반전 모드"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"향상된 대비 모드"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"색상 보정 모드"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"네트워크가\n모니터링될 수 있음"</string>
     <string name="description_target_search" msgid="3091587249776033139">"검색"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>하려면 위로 슬라이드"</string>
diff --git a/packages/SystemUI/res/values-lo-rLA/strings.xml b/packages/SystemUI/res/values-lo-rLA/strings.xml
index 59cf520..4503c4f 100644
--- a/packages/SystemUI/res/values-lo-rLA/strings.xml
+++ b/packages/SystemUI/res/values-lo-rLA/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"ໂໝດສະລັບສີ"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"ໂໝດຄວາມຕ່າງແສງ"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"ໂໝດການແກ້ໄຂສີ"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"ເຄືອຄ່າຍອາດ\nຖືກຕິດຕາມ"</string>
     <string name="description_target_search" msgid="3091587249776033139">"ຊອກຫາ"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"ເລື່ອນຂຶ້ນເພື່ອ <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-lt/strings.xml b/packages/SystemUI/res/values-lt/strings.xml
index 29ca84a9..1d8b051 100644
--- a/packages/SystemUI/res/values-lt/strings.xml
+++ b/packages/SystemUI/res/values-lt/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Spalvų inversijos režimas"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Patobulinto kontrasto režimas"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Spalvų taisymo režimas"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"PASTARIEJI"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Tinklas gali\nbūti stebimas"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Paieška"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Slyskite aukštyn link <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-lv/strings.xml b/packages/SystemUI/res/values-lv/strings.xml
index 4230b2e..603fa1a 100644
--- a/packages/SystemUI/res/values-lv/strings.xml
+++ b/packages/SystemUI/res/values-lv/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Krāsu inversijas režīms"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Uzlabota kontrasta režīms"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Krāsu korekcijas režīms"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"JAUNĀKIE"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Tīkls var\ntikt uzraudzīts"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Meklēt"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Velciet uz augšu, lai veiktu šādu darbību: <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-mn-rMN/strings.xml b/packages/SystemUI/res/values-mn-rMN/strings.xml
index d6c68bc..6deb896 100644
--- a/packages/SystemUI/res/values-mn-rMN/strings.xml
+++ b/packages/SystemUI/res/values-mn-rMN/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Өнгө урвуулах горим"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Сайжруулсан ялгаралтай горим"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Өнгө залруулах горим"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Сүлжээ хянагдаж\nбайж болзошгүй"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Хайх"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>-г гулсуулах."</string>
diff --git a/packages/SystemUI/res/values-ms-rMY/strings.xml b/packages/SystemUI/res/values-ms-rMY/strings.xml
index 114d03e..2150bea 100644
--- a/packages/SystemUI/res/values-ms-rMY/strings.xml
+++ b/packages/SystemUI/res/values-ms-rMY/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Mod penyongsangan warna"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Mod kontras dipertingkat"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Mod pembetulan warna"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Rangkaian mungkin\nboleh dipantau"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Carian"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Luncurkan ke atas untuk <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml
index 12deaef..433aa7c 100644
--- a/packages/SystemUI/res/values-nb/strings.xml
+++ b/packages/SystemUI/res/values-nb/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Modus for fargeinvertering"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Forbedret kontrastmodus"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Modus for fargekorrigering"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Nettverket kan\nvære overvåket"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Søk"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Dra opp for å <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-nl/strings.xml b/packages/SystemUI/res/values-nl/strings.xml
index 59c64b5..617aeb0 100644
--- a/packages/SystemUI/res/values-nl/strings.xml
+++ b/packages/SystemUI/res/values-nl/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Modus voor kleurinversie"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Modus voor verbeterd contrast"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Modus voor kleurcorrectie"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Netwerk kan\nworden gecontroleerd"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Zoeken"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Veeg omhoog voor <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-pl/strings.xml b/packages/SystemUI/res/values-pl/strings.xml
index b2628e1..c21230c 100644
--- a/packages/SystemUI/res/values-pl/strings.xml
+++ b/packages/SystemUI/res/values-pl/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Tryb odwrócenia kolorów"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Tryb zwiększonego kontrastu"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Tryb korekcji kolorów"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"OSTATNIE"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Sieć może być\nmonitorowana"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Szukaj"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Przesuń w górę: <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-pt-rPT/strings.xml b/packages/SystemUI/res/values-pt-rPT/strings.xml
index fc25ea8..d5c2e4c 100644
--- a/packages/SystemUI/res/values-pt-rPT/strings.xml
+++ b/packages/SystemUI/res/values-pt-rPT/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Modo de inversão de cor"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Modo de contraste melhorado"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Modo de correção de cor"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"A rede pode ser\nmonitorizada"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Pesquisar"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Deslize para cima para <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> ."</string>
diff --git a/packages/SystemUI/res/values-pt/strings.xml b/packages/SystemUI/res/values-pt/strings.xml
index e077fc6..21119e0 100644
--- a/packages/SystemUI/res/values-pt/strings.xml
+++ b/packages/SystemUI/res/values-pt/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Modo de inversão de cores"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Modo de contraste aprimorado"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Modo de correção de cor"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"A rede pode estar\nsob monitoração"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Pesquisar"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Para <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>, deslize para cima."</string>
diff --git a/packages/SystemUI/res/values-rm/strings.xml b/packages/SystemUI/res/values-rm/strings.xml
index adf54bc..33cb355 100644
--- a/packages/SystemUI/res/values-rm/strings.xml
+++ b/packages/SystemUI/res/values-rm/strings.xml
@@ -378,6 +378,8 @@
     <skip />
     <!-- no translation found for quick_settings_color_space_label (853443689745584770) -->
     <skip />
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <!-- no translation found for ssl_ca_cert_warning (9005954106902053641) -->
     <skip />
     <!-- no translation found for description_target_search (3091587249776033139) -->
diff --git a/packages/SystemUI/res/values-ro/strings.xml b/packages/SystemUI/res/values-ro/strings.xml
index 9cfb329..a8a7546 100644
--- a/packages/SystemUI/res/values-ro/strings.xml
+++ b/packages/SystemUI/res/values-ro/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Mod de inversare a culorilor"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Mod contrast îmbunătățit"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Mod de corectare a culorilor"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Rețeaua poate\nfi monitorizată"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Căutaţi"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Glisaţi în sus pentru <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-ru/strings.xml b/packages/SystemUI/res/values-ru/strings.xml
index 7a89d27..4aa67e7 100644
--- a/packages/SystemUI/res/values-ru/strings.xml
+++ b/packages/SystemUI/res/values-ru/strings.xml
@@ -208,6 +208,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Инверсия цвета"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Контрастность"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Коррекция цвета"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Действия в сети\nмогут отслеживаться"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Поиск"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Проведите вверх, чтобы <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-sk/strings.xml b/packages/SystemUI/res/values-sk/strings.xml
index 13c60946..bc6a2f2 100644
--- a/packages/SystemUI/res/values-sk/strings.xml
+++ b/packages/SystemUI/res/values-sk/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Režim prevrátenia farieb"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Režim zvýšeného kontrastu"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Režim korekcie farieb"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Sieť môže byť\nmonitorovaná"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Vyhľadávanie"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Prejdite prstom nahor: <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-sl/strings.xml b/packages/SystemUI/res/values-sl/strings.xml
index a5b3244..7018188 100644
--- a/packages/SystemUI/res/values-sl/strings.xml
+++ b/packages/SystemUI/res/values-sl/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Način inverzije barv"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Način izboljšanega kontrasta"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Način popravljanja barv"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Omrežje je\nlahko spremljano"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Iskanje"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Povlecite navzgor za <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-sr/strings.xml b/packages/SystemUI/res/values-sr/strings.xml
index 0bdccee..89bc3c5 100644
--- a/packages/SystemUI/res/values-sr/strings.xml
+++ b/packages/SystemUI/res/values-sr/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Режим инверзије боје"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Режим унапређеног контраста"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Режим корекције боје"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"НАЈНОВИЈЕ"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Мрежа се можда\nнадгледа"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Претрага"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Превуците нагоре за <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-sv/strings.xml b/packages/SystemUI/res/values-sv/strings.xml
index efe7fcb..1b47be5 100644
--- a/packages/SystemUI/res/values-sv/strings.xml
+++ b/packages/SystemUI/res/values-sv/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Färginverteringsläge"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Kontrastförbättringsläge"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Färgkorrigeringsläge"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"NYA"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Nätverket kan\nvara övervakat"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Sök"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Dra uppåt för <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> ."</string>
diff --git a/packages/SystemUI/res/values-sw/strings.xml b/packages/SystemUI/res/values-sw/strings.xml
index 9336ade..08f8012 100644
--- a/packages/SystemUI/res/values-sw/strings.xml
+++ b/packages/SystemUI/res/values-sw/strings.xml
@@ -202,6 +202,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Hali ya ugeuzaji kinyume wa rangi"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Hali ya utofautishaji ulioboreshwa"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Hali ya kusahihisha rangi"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Huenda mtandao\nunafuatiliwa"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Tafuta"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Sogeza juu kwa <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> ."</string>
diff --git a/packages/SystemUI/res/values-th/strings.xml b/packages/SystemUI/res/values-th/strings.xml
index 033e56d..98102dc 100644
--- a/packages/SystemUI/res/values-th/strings.xml
+++ b/packages/SystemUI/res/values-th/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"โหมดการกลับสี"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"โหมดคอนทราสต์ที่ปรับปรุงแล้ว"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"โหมดการแก้ไขสี"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"เครือข่ายอาจ\nถูกตรวจสอบ"</string>
     <string name="description_target_search" msgid="3091587249776033139">"ค้นหา"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"เลื่อนขึ้นเพื่อ <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>"</string>
diff --git a/packages/SystemUI/res/values-tl/strings.xml b/packages/SystemUI/res/values-tl/strings.xml
index 3704c48..403fc5f 100644
--- a/packages/SystemUI/res/values-tl/strings.xml
+++ b/packages/SystemUI/res/values-tl/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Mode ng pag-invert ng kulay"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Mode na dinagdagan ang contrast"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Mode ng pagtatama ng kulay"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Maaaring\nsinusubaybayan ang network"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Maghanap"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Mag-slide pataas para sa <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-tr/strings.xml b/packages/SystemUI/res/values-tr/strings.xml
index 56e5c88..c2f0d82 100644
--- a/packages/SystemUI/res/values-tr/strings.xml
+++ b/packages/SystemUI/res/values-tr/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Renk ters çevirme modu"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Geliştirilmiş kontrast modu"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Renk düzeltme modu"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Ağ izleniyor\nolabilir"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Ara"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g> için yukarı kaydırın."</string>
diff --git a/packages/SystemUI/res/values-uk/strings.xml b/packages/SystemUI/res/values-uk/strings.xml
index 3a474a4..9f7b961 100644
--- a/packages/SystemUI/res/values-uk/strings.xml
+++ b/packages/SystemUI/res/values-uk/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Режим інверсії кольорів"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Режим посиленого контрасту"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Режим коригування кольору"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Мережа може\nвідстежуватися"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Пошук"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Проведіть пальцем угору, щоб <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-vi/strings.xml b/packages/SystemUI/res/values-vi/strings.xml
index 700c20d..96bc730 100644
--- a/packages/SystemUI/res/values-vi/strings.xml
+++ b/packages/SystemUI/res/values-vi/strings.xml
@@ -204,6 +204,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Chế độ đảo ngược màu sắc"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Chế độ tương phản tăng cường"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Chế độ hiệu chỉnh màu sắc"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Mạng có thể\nđược giám sát"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Tìm kiếm"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Trượt lên để <xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/res/values-zh-rCN/strings.xml b/packages/SystemUI/res/values-zh-rCN/strings.xml
index 5909fbf..163c424 100644
--- a/packages/SystemUI/res/values-zh-rCN/strings.xml
+++ b/packages/SystemUI/res/values-zh-rCN/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"颜色反转模式"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"增强对比度模式"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"颜色校正模式"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"网络可能会\n受到监控"</string>
     <string name="description_target_search" msgid="3091587249776033139">"搜索"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"向上滑动以<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>。"</string>
diff --git a/packages/SystemUI/res/values-zh-rHK/strings.xml b/packages/SystemUI/res/values-zh-rHK/strings.xml
index d229347..88a39ac 100644
--- a/packages/SystemUI/res/values-zh-rHK/strings.xml
+++ b/packages/SystemUI/res/values-zh-rHK/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"色彩反轉模式"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"增強對比模式"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"色彩校準模式"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"網絡可能會\n受到監控"</string>
     <string name="description_target_search" msgid="3091587249776033139">"搜尋"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"向上滑動即可<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>。"</string>
diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml
index bf2d7ce..9a59c78 100644
--- a/packages/SystemUI/res/values-zh-rTW/strings.xml
+++ b/packages/SystemUI/res/values-zh-rTW/strings.xml
@@ -206,6 +206,8 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"彩色反轉模式"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"增強對比模式"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"色彩校正模式"</string>
+    <!-- no translation found for recents_empty_message (2269156590813544104) -->
+    <skip />
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"網路可能\n受到監控"</string>
     <string name="description_target_search" msgid="3091587249776033139">"搜尋"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"向上滑動即可<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>。"</string>
diff --git a/packages/SystemUI/res/values-zu/strings.xml b/packages/SystemUI/res/values-zu/strings.xml
index d676b5c..92e3382 100644
--- a/packages/SystemUI/res/values-zu/strings.xml
+++ b/packages/SystemUI/res/values-zu/strings.xml
@@ -204,6 +204,7 @@
     <string name="quick_settings_inversion_label" msgid="1666358784283020762">"Imodi yokuguqulwa kombala"</string>
     <string name="quick_settings_contrast_label" msgid="3319507551689108692">"Imodi ethuthukisiwe yokugqama"</string>
     <string name="quick_settings_color_space_label" msgid="853443689745584770">"Imodi yokulungisa umbala"</string>
+    <string name="recents_empty_message" msgid="2269156590813544104">"OKWAKAMUVA"</string>
     <string name="ssl_ca_cert_warning" msgid="9005954106902053641">"Kungenzeka inethiwekhi\niqashiwe"</string>
     <string name="description_target_search" msgid="3091587249776033139">"Sesha"</string>
     <string name="description_direction_up" msgid="7169032478259485180">"Shelelisela ngenhla ku-<xliff:g id="TARGET_DESCRIPTION">%s</xliff:g>."</string>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index eb07d88..bd36128 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -28,6 +28,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
@@ -46,6 +47,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamManager;
@@ -55,6 +57,7 @@
 import android.text.TextUtils;
 import android.text.style.TextAppearanceSpan;
 import android.util.Log;
+import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.view.ContextThemeWrapper;
 import android.view.Display;
@@ -140,6 +143,7 @@
     protected PopupMenu mNotificationBlamePopup;
 
     protected int mCurrentUserId = 0;
+    final protected SparseArray<UserInfo> mRelatedUsers = new SparseArray<UserInfo>();
 
     protected int mLayoutDirection = -1; // invalid
     private Locale mLocale;
@@ -156,6 +160,8 @@
     private Context mLightThemeContext;
     private ImageUtils mImageUtils = new ImageUtils();
 
+    private UserManager mUserManager;
+
     // UI-specific methods
 
     /**
@@ -248,12 +254,26 @@
             String action = intent.getAction();
             if (Intent.ACTION_USER_SWITCHED.equals(action)) {
                 mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                updateRelatedUserCache();
                 if (true) Log.v(TAG, "userId " + mCurrentUserId + " is in the house");
                 userSwitched(mCurrentUserId);
+            } else if (Intent.ACTION_USER_ADDED.equals(action)) {
+                updateRelatedUserCache();
             }
         }
     };
 
+    private void updateRelatedUserCache() {
+        synchronized (mRelatedUsers) {
+            mRelatedUsers.clear();
+            if (mUserManager != null) {
+                for (UserInfo related : mUserManager.getRelatedUsers(mCurrentUserId)) {
+                    mRelatedUsers.put(related.id, related);
+                }
+            }
+        }
+    }
+
     public void start() {
         mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
         mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
@@ -287,6 +307,8 @@
         mLocale = mContext.getResources().getConfiguration().locale;
         mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(mLocale);
 
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+
         // Connect in to the status bar manager service
         StatusBarIconList iconList = new StatusBarIconList();
         ArrayList<IBinder> notificationKeys = new ArrayList<IBinder>();
@@ -348,22 +370,28 @@
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_USER_SWITCHED);
+        filter.addAction(Intent.ACTION_USER_ADDED);
         mContext.registerReceiver(mBroadcastReceiver, filter);
+
+        updateRelatedUserCache();
     }
 
     public void userSwitched(int newUserId) {
         // should be overridden
     }
 
-    public boolean notificationIsForCurrentUser(StatusBarNotification n) {
+    public boolean notificationIsForCurrentOrRelatedUser(StatusBarNotification n) {
         final int thisUserId = mCurrentUserId;
         final int notificationUserId = n.getUserId();
         if (DEBUG && MULTIUSER_DEBUG) {
             Log.v(TAG, String.format("%s: current userid: %d, notification userid: %d",
                     n, thisUserId, notificationUserId));
         }
-        return notificationUserId == UserHandle.USER_ALL
-                || thisUserId == notificationUserId;
+        synchronized (mRelatedUsers) {
+            return notificationUserId == UserHandle.USER_ALL
+                    || thisUserId == notificationUserId
+                    || mRelatedUsers.get(notificationUserId) != null;
+        }
     }
 
     @Override
@@ -389,13 +417,14 @@
             final String _pkg = n.getPackageName();
             final String _tag = n.getTag();
             final int _id = n.getId();
+            final int _userId = n.getUserId();
             vetoButton.setOnClickListener(new View.OnClickListener() {
                     public void onClick(View v) {
                         // Accessibility feedback
                         v.announceForAccessibility(
                                 mContext.getString(R.string.accessibility_notification_dismissed));
                         try {
-                            mBarService.onNotificationClear(_pkg, _tag, _id);
+                            mBarService.onNotificationClear(_pkg, _tag, _id, _userId);
 
                         } catch (RemoteException ex) {
                             // system process is dead if we're here.
@@ -907,7 +936,7 @@
         PendingIntent contentIntent = sbn.getNotification().contentIntent;
         if (contentIntent != null) {
             final View.OnClickListener listener = makeClicker(contentIntent,
-                    sbn.getPackageName(), sbn.getTag(), sbn.getId(), isHeadsUp);
+                    sbn.getPackageName(), sbn.getTag(), sbn.getId(), isHeadsUp, sbn.getUserId());
             content.setOnClickListener(listener);
         } else {
             content.setOnClickListener(null);
@@ -931,6 +960,7 @@
         }
 
         if (contentViewLocal != null) {
+            contentViewLocal.setIsRootNamespace(true);
             SizeAdaptiveLayout.LayoutParams params =
                     new SizeAdaptiveLayout.LayoutParams(contentViewLocal.getLayoutParams());
             params.minHeight = minHeight;
@@ -938,6 +968,7 @@
             expanded.addView(contentViewLocal, params);
         }
         if (bigContentViewLocal != null) {
+            bigContentViewLocal.setIsRootNamespace(true);
             SizeAdaptiveLayout.LayoutParams params =
                     new SizeAdaptiveLayout.LayoutParams(bigContentViewLocal.getLayoutParams());
             params.minHeight = minHeight+1;
@@ -955,6 +986,7 @@
                         expandedPublic, mOnClickHandler);
 
                 if (publicViewLocal != null) {
+                    publicViewLocal.setIsRootNamespace(true);
                     SizeAdaptiveLayout.LayoutParams params =
                             new SizeAdaptiveLayout.LayoutParams(publicViewLocal.getLayoutParams());
                     params.minHeight = minHeight;
@@ -1014,7 +1046,7 @@
             TextView debug = (TextView) row.findViewById(R.id.debug_info);
             if (debug != null) {
                 debug.setVisibility(View.VISIBLE);
-                debug.setText("U " + entry.notification.getUserId());
+                debug.setText("CU " + mCurrentUserId +" NU " + entry.notification.getUserId());
             }
         }
         entry.row = row;
@@ -1027,9 +1059,9 @@
         return true;
     }
 
-    public NotificationClicker makeClicker(PendingIntent intent, String pkg, String tag, int id,
-            boolean forHun) {
-        return new NotificationClicker(intent, pkg, tag, id, forHun);
+    public NotificationClicker makeClicker(PendingIntent intent, String pkg, String tag,
+            int id, boolean forHun, int userId) {
+        return new NotificationClicker(intent, pkg, tag, id, forHun, userId);
     }
 
     protected class NotificationClicker implements View.OnClickListener {
@@ -1038,14 +1070,16 @@
         private String mTag;
         private int mId;
         private boolean mIsHeadsUp;
+        private int mUserId;
 
         public NotificationClicker(PendingIntent intent, String pkg, String tag, int id,
-                boolean forHun) {
+                boolean forHun, int userId) {
             mIntent = intent;
             mPkg = pkg;
             mTag = tag;
             mId = id;
             mIsHeadsUp = forHun;
+            mUserId = userId;
         }
 
         public void onClick(View v) {
@@ -1081,7 +1115,7 @@
                 if (mIsHeadsUp) {
                     mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP);
                 }
-                mBarService.onNotificationClick(mPkg, mTag, mId);
+                mBarService.onNotificationClick(mPkg, mTag, mId, mUserId);
             } catch (RemoteException ex) {
                 // system process is dead if we're here.
             }
@@ -1119,7 +1153,8 @@
     void handleNotificationError(IBinder key, StatusBarNotification n, String message) {
         removeNotification(key);
         try {
-            mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(), n.getInitialPid(), message);
+            mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(),
+                    n.getInitialPid(), message, n.getUserId());
         } catch (RemoteException ex) {
             // The end is nigh.
         }
@@ -1388,7 +1423,7 @@
         updateNotificationVetoButton(oldEntry.row, notification);
 
         // Is this for you?
-        boolean isForCurrentUser = notificationIsForCurrentUser(notification);
+        boolean isForCurrentUser = notificationIsForCurrentOrRelatedUser(notification);
         if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
 
         // Restart the ticker if it's still running
@@ -1440,7 +1475,7 @@
         if (contentIntent != null) {
             final View.OnClickListener listener = makeClicker(contentIntent,
                     notification.getPackageName(), notification.getTag(), notification.getId(),
-                    isHeadsUp);
+                    isHeadsUp, notification.getUserId());
             entry.content.setOnClickListener(listener);
         } else {
             entry.content.setOnClickListener(null);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 237b7f7..6be6d4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -20,7 +20,6 @@
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
-import android.provider.Settings;
 import android.util.AttributeSet;
 import android.util.EventLog;
 import android.view.MotionEvent;
@@ -57,17 +56,6 @@
         mHandleBar = resources.getDrawable(R.drawable.status_bar_close);
         mHandleBarHeight = resources.getDimensionPixelSize(R.dimen.close_handle_height);
         mHandleView = findViewById(R.id.handle);
-        PanelHeaderView header = (PanelHeaderView) findViewById(R.id.header);
-        ZenModeView zenModeView = (ZenModeView) findViewById(R.id.zenmode);
-        zenModeView.setAdapter(new ZenModeViewAdapter(mContext) {
-            @Override
-            public void configure() {
-                if (mStatusBar != null) {
-                    mStatusBar.startSettingsActivity(Settings.ACTION_ZEN_MODE_SETTINGS);
-                }
-            }
-        });
-        header.setZenModeView(zenModeView);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelHeaderView.java
deleted file mode 100644
index a28324d..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelHeaderView.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.widget.LinearLayout;
-
-public class PanelHeaderView extends LinearLayout {
-    private static final String TAG = "PanelHeaderView";
-    private static final boolean DEBUG = false;
-
-    private ZenModeView mZenModeView;
-
-    public PanelHeaderView(Context context) {
-        super(context);
-    }
-
-    public PanelHeaderView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public void setZenModeView(ZenModeView zmv) {
-        mZenModeView = zmv;
-    }
-
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        final boolean rt = super.dispatchTouchEvent(ev);
-        if (DEBUG) logTouchEvent("dispatchTouchEvent", rt, ev);
-        if (mZenModeView != null) {
-            mZenModeView.dispatchExternalTouchEvent(ev);
-        }
-        return rt;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        final boolean rt = super.onInterceptTouchEvent(ev);
-        if (DEBUG) logTouchEvent("onInterceptTouchEvent", rt, ev);
-        return rt;
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        boolean rt = super.onTouchEvent(event);
-        if (DEBUG) logTouchEvent("onTouchEvent", rt, event);
-        return true;
-    }
-
-    private void logTouchEvent(String method, boolean rt, MotionEvent ev) {
-        Log.d(TAG, method + " " + (rt ? "TRUE" : "FALSE") + " " + ev);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 6718de1..9540bd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -1056,7 +1056,10 @@
         for (int i=0; i<N; i++) {
             Entry ent = mNotificationData.get(N-i-1);
             if (!(provisioned || showNotificationEvenIfUnprovisioned(ent.notification))) continue;
-            if (!notificationIsForCurrentUser(ent.notification)) continue;
+
+            // TODO How do we want to badge notifcations from related users.
+            if (!notificationIsForCurrentOrRelatedUser(ent.notification)) continue;
+
             final int vis = ent.notification.getNotification().visibility;
             if (vis != Notification.VISIBILITY_SECRET) {
                 // when isLockscreenPublicMode() we show the public form of VISIBILITY_PRIVATE notifications
@@ -1114,7 +1117,7 @@
             Entry ent = mNotificationData.get(N-i-1);
             if (!((provisioned && ent.notification.getScore() >= HIDE_ICONS_BELOW_SCORE)
                     || showNotificationEvenIfUnprovisioned(ent.notification))) continue;
-            if (!notificationIsForCurrentUser(ent.notification)) continue;
+            if (!notificationIsForCurrentOrRelatedUser(ent.notification)) continue;
             if (isLockscreenPublicMode()
                     && ent.notification.getNotification().visibility
                             == Notification.VISIBILITY_SECRET
@@ -2121,7 +2124,7 @@
         if (!isDeviceProvisioned()) return;
 
         // not for you
-        if (!notificationIsForCurrentUser(n)) return;
+        if (!notificationIsForCurrentOrRelatedUser(n)) return;
 
         // Show the ticker if one is requested. Also don't do this
         // until status bar window is attached to the window manager,
@@ -2429,7 +2432,7 @@
                                 }
                                 try {
                                     mPile.setViewRemoval(true);
-                                    mBarService.onClearAllNotifications();
+                                    mBarService.onClearAllNotifications(mCurrentUserId);
                                 } catch (Exception ex) { }
                             }
                         };
@@ -2607,7 +2610,8 @@
                 mBarService.onNotificationClear(
                         mInterruptingNotificationEntry.notification.getPackageName(),
                         mInterruptingNotificationEntry.notification.getTag(),
-                        mInterruptingNotificationEntry.notification.getId());
+                        mInterruptingNotificationEntry.notification.getId(),
+                        mInterruptingNotificationEntry.notification.getUserId());
             } catch (android.os.RemoteException ex) {
                 // oh well
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java
index c1c8946..8170b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java
@@ -52,13 +52,16 @@
 import android.provider.ContactsContract.Profile;
 import android.provider.Settings;
 import android.security.KeyChain;
+import android.text.TextUtils.TruncateAt;
 import android.util.Log;
 import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.Window;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
+import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.TextView;
 
@@ -586,6 +589,31 @@
         });
         parent.addView(airplaneTile);
 
+        // Zen Mode
+        final QuickSettingsBasicTile zenModeTile = new QuickSettingsBasicTile(mContext);
+        zenModeTile.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showZenModeDialog();
+            }
+        });
+        mModel.addZenModeTile(zenModeTile, new QuickSettingsModel.RefreshCallback() {
+            @Override
+            public void refreshView(QuickSettingsTileView unused, State state) {
+                zenModeTile.setImageResource(state.iconId);
+                // TODO cut new assets
+                zenModeTile.getImageView().setAlpha(state.enabled ? 1 : .2f);
+                zenModeTile.getImageView().setScaleX(1.5f);
+                zenModeTile.getImageView().setScaleY(1.5f);
+                // for landscape version
+                zenModeTile.getTextView().setMaxLines(2);
+                zenModeTile.getTextView().setEllipsize(TruncateAt.END);
+                // TODO content description
+                zenModeTile.setText(state.label);
+            }
+        });
+        parent.addView(zenModeTile);
+
         // Bluetooth
         if (mModel.deviceSupportsBluetooth()
                 || DEBUG_GONE_TILES) {
@@ -864,6 +892,31 @@
         dialog.show();
     }
 
+    private void showZenModeDialog() {
+        final Dialog d = new Dialog(mContext);
+        d.requestWindowFeature(Window.FEATURE_NO_TITLE);
+        d.setCancelable(true);
+        d.setCanceledOnTouchOutside(true);
+        final ZenModeView v = new ZenModeView(mContext);
+        v.setAdapter(new ZenModeViewAdapter(mContext) {
+            @Override
+            public void configure() {
+                if (mStatusBarService != null) {
+                    mStatusBarService.startSettingsActivity(Settings.ACTION_ZEN_MODE_SETTINGS);
+                }
+                d.dismiss();
+            }
+            @Override
+            public void close() {
+                d.dismiss();
+            }
+        });
+        d.setContentView(v);
+        d.create();
+        d.getWindow().setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
+        d.show();
+    }
+
     private void applyBluetoothStatus() {
         mModel.onBluetoothStateChange(mBluetoothState);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java
index 174cad8..c3c281c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java
@@ -38,6 +38,7 @@
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.WindowManager;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.InputMethodSubtype;
@@ -112,6 +113,9 @@
     public static class RotationLockState extends State {
         boolean visible = false;
     }
+    public static class ZenModeState extends State {
+        int zenMode = Settings.Global.ZEN_MODE_OFF;
+    }
 
     /** The callback to update a given tile. */
     interface RefreshCallback {
@@ -294,6 +298,25 @@
         }
     }
 
+    /** ContentObserver to watch display color space adjustment */
+    private class ZenModeObserver extends ContentObserver {
+        public ZenModeObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            onZenModeChanged();
+        }
+
+        public void startObserving() {
+            final ContentResolver cr = mContext.getContentResolver();
+            cr.unregisterContentObserver(this);
+            cr.registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false, this);
+        }
+    }
+
     /** Callback for changes to remote display routes. */
     private class RemoteDisplayRouteCallback extends MediaRouter.SimpleCallback {
         @Override
@@ -327,6 +350,7 @@
     private final DisplayInversionObserver mInversionObserver;
     private final DisplayContrastObserver mContrastObserver;
     private final DisplayColorSpaceObserver mColorSpaceObserver;
+    private final ZenModeObserver mZenModeObserver;
 
     private final MediaRouter mMediaRouter;
     private final RemoteDisplayRouteCallback mRemoteDisplayRouteCallback;
@@ -349,6 +373,10 @@
     private RefreshCallback mAirplaneModeCallback;
     private State mAirplaneModeState = new State();
 
+    private QuickSettingsTileView mZenModeTile;
+    private RefreshCallback mZenModeCallback;
+    private ZenModeState mZenModeState = new ZenModeState();
+
     private QuickSettingsTileView mWifiTile;
     private RefreshCallback mWifiCallback;
     private WifiState mWifiState = new WifiState();
@@ -445,6 +473,8 @@
         mContrastObserver.startObserving();
         mColorSpaceObserver = new DisplayColorSpaceObserver(mHandler);
         mColorSpaceObserver.startObserving();
+        mZenModeObserver = new ZenModeObserver(mHandler);
+        mZenModeObserver.startObserving();
 
         mMediaRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
         rebindMediaRouterAsCurrentUser();
@@ -567,6 +597,30 @@
         mAirplaneModeCallback.refreshView(mAirplaneModeTile, mAirplaneModeState);
     }
 
+    // Zen Mode
+    void addZenModeTile(QuickSettingsTileView view, RefreshCallback cb) {
+        mZenModeTile = view;
+        mZenModeCallback = cb;
+        onZenModeChanged();
+    }
+    private void onZenModeChanged() {
+        final int mode = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF);
+        mZenModeState.enabled = mode != Settings.Global.ZEN_MODE_OFF;
+        mZenModeState.zenMode = mode;
+        if (mode == Settings.Global.ZEN_MODE_FULL) {
+            mZenModeState.iconId = R.drawable.stat_sys_zen_full;
+            mZenModeState.label = ZenModeView.modeToLabel(ZenModeView.Adapter.MODE_FULL);
+        } else if (mode == Settings.Global.ZEN_MODE_LIMITED) {
+            mZenModeState.iconId = R.drawable.stat_sys_zen_limited;
+            mZenModeState.label = ZenModeView.modeToLabel(ZenModeView.Adapter.MODE_LIMITED);
+        } else {
+            mZenModeState.iconId = R.drawable.stat_sys_zen_limited;
+            mZenModeState.label = ZenModeView.modeToLabel(ZenModeView.Adapter.MODE_LIMITED);
+        }
+        mZenModeCallback.refreshView(mZenModeTile, mZenModeState);
+    }
+
     // Wifi
     void addWifiTile(QuickSettingsTileView view, RefreshCallback cb) {
         mWifiTile = view;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java
index fa7f96a..d1c7a41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java
@@ -19,22 +19,16 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.content.Context;
-import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Path;
-import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.ShapeDrawable;
 import android.graphics.drawable.shapes.PathShape;
-import android.os.AsyncTask;
-import android.os.Vibrator;
 import android.text.Spannable;
 import android.text.SpannableStringBuilder;
 import android.text.TextPaint;
 import android.text.method.LinkMovementMethod;
-import android.text.style.RelativeSizeSpan;
 import android.text.style.URLSpan;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -62,7 +56,7 @@
     private static final Typeface CONDENSED =
             Typeface.create("sans-serif-condensed", Typeface.NORMAL);
     private static final int GRAY = 0xff999999; //TextAppearance.StatusBar.Expanded.Network
-    private static final int BACKGROUND = 0xff1d3741; //0x3333b5e5;
+    private static final int BACKGROUND = 0xff282828;
     private static final long DURATION = new ValueAnimator().getDuration();
     private static final long BOUNCE_DURATION = DURATION / 3;
     private static final float BOUNCE_SCALE = 0.8f;
@@ -73,23 +67,14 @@
 
     private final Context mContext;
     private final Paint mPathPaint;
-    private final TextView mHintText;
-    private final ModeSpinner mModeSpinner;
-    private final ImageView mCloseButton;
     private final ImageView mSettingsButton;
-    private final Rect mLayoutRect = new Rect();
+    private final ModeSpinner mModeSpinner;
+    private final TextView mActionButton;
+    private final View mDivider;
     private final UntilPager mUntilPager;
     private final AlarmWarning mAlarmWarning;
-    private final int mPopDuration;
 
-    private float mDownY;
-    private int mDownBottom;
-    private boolean mPeekable = true;
-    private boolean mClosing;
-    private int mBottom;
-    private int mWidthSpec;
     private Adapter mAdapter;
-    private boolean mPopped;
 
     public ZenModeView(Context context) {
         this(context, null);
@@ -100,34 +85,22 @@
         if (DEBUG) log("new %s()", getClass().getSimpleName());
         mContext = context;
 
-        mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mPathPaint.setStyle(Paint.Style.STROKE);
-        mPathPaint.setColor(GRAY);
-        mPathPaint.setStrokeWidth(5);
-
         final int iconSize = mContext.getResources()
                 .getDimensionPixelSize(com.android.internal.R.dimen.notification_large_icon_width);
         final int topRowSize = iconSize * 2 / 3;
+        final int p = topRowSize / 7;
 
-        mCloseButton = new ImageView(mContext);
-        mCloseButton.setAlpha(0f);
-        mCloseButton.setImageDrawable(sd(closePath(topRowSize), topRowSize, mPathPaint));
-        addView(mCloseButton, new LayoutParams(topRowSize, topRowSize));
-        mCloseButton.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                bounce(v, null);
-                close();
-            }
-        });
+        mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mPathPaint.setStyle(Paint.Style.STROKE);
+        mPathPaint.setColor(GRAY);
+        mPathPaint.setStrokeWidth(p / 2);
 
         mSettingsButton = new ImageView(mContext);
-        mSettingsButton.setAlpha(0f);
-        final int p = topRowSize / 7;
         mSettingsButton.setPadding(p, p, p, p);
         mSettingsButton.setImageResource(R.drawable.ic_notify_settings_normal);
         LayoutParams lp = new LayoutParams(topRowSize, topRowSize);
-        lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+        lp.topMargin = p;
+        lp.leftMargin = p;
         addView(mSettingsButton, lp);
         mSettingsButton.setOnClickListener(new View.OnClickListener() {
             @Override
@@ -140,65 +113,65 @@
         });
 
         mModeSpinner = new ModeSpinner(mContext);
-        mModeSpinner.setAlpha(0);
-        mModeSpinner.setEnabled(false);
         mModeSpinner.setId(android.R.id.title);
         lp = new LayoutParams(LayoutParams.WRAP_CONTENT, topRowSize);
-        lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
+        lp.topMargin = p;
+        lp.addRule(CENTER_HORIZONTAL);
         addView(mModeSpinner, lp);
 
-        mUntilPager = new UntilPager(mContext, mPathPaint, iconSize);
-        mUntilPager.setId(android.R.id.tabhost);
-        mUntilPager.setAlpha(0);
-        lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+        mActionButton = new TextView(mContext);
+        mActionButton.setTextColor(GRAY);
+        mActionButton.setTypeface(CONDENSED);
+        mActionButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, mActionButton.getTextSize() * 1.2f);
+        mActionButton.setAllCaps(true);
+        mActionButton.setGravity(Gravity.CENTER);
+        mActionButton.setPadding(p, 0, p * 2, 0);
+        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, topRowSize);
+        lp.topMargin = p;
+        lp.addRule(ALIGN_PARENT_RIGHT);
+        lp.addRule(ALIGN_BASELINE, mModeSpinner.getId());
+        addView(mActionButton, lp);
+        mActionButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                bounce(v, null);
+                beginOrEnd();
+            }
+        });
+
+        mDivider = new View(mContext);
+        mDivider.setId(android.R.id.empty);
+        mDivider.setBackgroundColor(GRAY);
+        lp = new LayoutParams(LayoutParams.MATCH_PARENT, 2);
         lp.addRule(BELOW, mModeSpinner.getId());
+        lp.topMargin = p;
+        lp.bottomMargin = p;
+        addView(mDivider, lp);
+
+        mUntilPager = new UntilPager(mContext, mPathPaint, iconSize * 3 / 4);
+        mUntilPager.setId(android.R.id.tabhost);
+        lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+        lp.addRule(BELOW, mDivider.getId());
         addView(mUntilPager, lp);
 
         mAlarmWarning = new AlarmWarning(mContext);
-        mAlarmWarning.setAlpha(0);
         lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
         lp.addRule(CENTER_HORIZONTAL);
         lp.addRule(BELOW, mUntilPager.getId());
+        lp.bottomMargin = p;
         addView(mAlarmWarning, lp);
-
-        mHintText = new TextView(mContext);
-        mHintText.setTypeface(CONDENSED);
-        mHintText.setText("Swipe down for Limited Interruptions");
-        mHintText.setGravity(Gravity.CENTER);
-        mHintText.setTextColor(GRAY);
-        addView(mHintText, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
-
-        mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms);
     }
 
-    private boolean isApplicable() {
-        return mAdapter != null && mAdapter.isApplicable();
-    }
-
-    private void close() {
-        mClosing = true;
-        final int startBottom = mBottom;
-        final int max = mPeekable ? getExpandedBottom() : startBottom;
-        mHintText.animate().alpha(1).setUpdateListener(new AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                final float f = animation.getAnimatedFraction();
-                final int hintBottom = mHintText.getBottom();
-                final boolean isDone = f == 1;
-                setPeeked(hintBottom + (int)((1-f) * (startBottom - hintBottom)), max, isDone);
-                if (isDone) {
-                    mPeekable = true;
-                    mPopped = false;
-                    mClosing = false;
-                    mModeSpinner.updateState();
-                    if (mAdapter != null) {
-                        mAdapter.cancel();
-                    }
-                }
-            }
-        }).start();
-        mUntilPager.animate().alpha(0).start();
-        mAlarmWarning.animate().alpha(0).start();
+    private void beginOrEnd() {
+        if (mAdapter == null) return;
+        if (mAdapter.getMode() == mAdapter.getCommittedMode()) {
+            // end
+            mAdapter.setCommittedMode(Adapter.MODE_OFF);
+        } else {
+            // begin
+            mAdapter.setCommittedMode(mAdapter.getMode());
+        }
+        mAdapter.close();
     }
 
     public void setAdapter(Adapter adapter) {
@@ -218,180 +191,41 @@
     }
 
     private void updateState(boolean animate) {
-        final boolean applicable = isApplicable();
-        setVisibility(applicable ? VISIBLE : GONE);
-        if (!applicable) {
-            return;
-        }
-        if (mAdapter != null && mAdapter.getMode() == Adapter.MODE_OFF && !mPeekable) {
-            close();
-        } else {
-            mModeSpinner.updateState();
-            mUntilPager.updateState();
-            mAlarmWarning.updateState(animate);
-            final float settingsAlpha = getSettingsButtonAlpha();
-            if (settingsAlpha != mSettingsButton.getAlpha()) {
-                if (animate) {
-                    mSettingsButton.animate().alpha(settingsAlpha).start();
-                } else {
-                    mSettingsButton.setAlpha(settingsAlpha);
-                }
-            }
-            if (mPeekable && mAdapter != null && mAdapter.getMode() != Adapter.MODE_OFF) {
-                if (DEBUG) log("panic expand!");
-                mPeekable = false;
-                mModeSpinner.setEnabled(true);
-                mBottom = getExpandedBottom();
-                setExpanded(1);
+        mModeSpinner.updateState();
+        mUntilPager.updateState();
+        mAlarmWarning.updateState(animate);
+        final float settingsAlpha = isFull() ? 0 : SETTINGS_ALPHA;
+        if (settingsAlpha != mSettingsButton.getAlpha()) {
+            if (animate) {
+                mSettingsButton.animate().alpha(settingsAlpha).start();
+            } else {
+                mSettingsButton.setAlpha(settingsAlpha);
             }
         }
+        final boolean committed = mAdapter != null
+                && mAdapter.getMode() == mAdapter.getCommittedMode();
+        mActionButton.setText(committed ? "End" : "Begin");
     }
 
-    private float getSettingsButtonAlpha() {
-        final boolean full = mAdapter != null && mAdapter.getMode() == Adapter.MODE_FULL;
-        final boolean collapsed = mHintText.getAlpha() == 1;
-        return full || collapsed ? 0 : SETTINGS_ALPHA;
-    }
-
-    private static Path closePath(int size) {
-        final int pad = size / 4;
-        final Path p = new Path();
-        p.moveTo(pad, pad);
-        p.lineTo(size - pad, size - pad);
-        p.moveTo(size - pad, pad);
-        p.lineTo(pad, size - pad);
-        return p;
+    private boolean isFull() {
+        return mAdapter != null && mAdapter.getMode() == Adapter.MODE_FULL;
     }
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         if (DEBUG) log("onMeasure %s %s",
                 MeasureSpec.toString(widthMeasureSpec), MeasureSpec.toString(heightMeasureSpec));
-        final boolean widthExact = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
-
-        if (!widthExact || (widthMeasureSpec != mWidthSpec)) {
-            if (DEBUG) log("  super.onMeasure");
-            final int hms = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
-            super.onMeasure(widthMeasureSpec, hms);
-            mBottom = mPeekable ? mHintText.getMeasuredHeight() : getExpandedBottom();
-            mWidthSpec = widthMeasureSpec;
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        if (!isFull()) {
+            final LayoutParams lp = (LayoutParams) mModeSpinner.getLayoutParams();
+            final int mh = vh(mModeSpinner) + vh(mDivider) + vh(mUntilPager) + lp.topMargin;
+            setMeasuredDimension(getMeasuredWidth(), mh);
         }
-        if (DEBUG) log("mBottom (OM) = " + mBottom);
-        setMeasuredDimension(getMeasuredWidth(), mBottom);
-        if (DEBUG) log("  mw=%s mh=%s",
-                toString(getMeasuredWidthAndState()), toString(getMeasuredHeightAndState()));
     }
 
-    private static String toString(int sizeAndState) {
-        final int size = sizeAndState & MEASURED_SIZE_MASK;
-        final boolean tooSmall = (sizeAndState & MEASURED_STATE_TOO_SMALL) != 0;
-        return size + (tooSmall ? "TOO SMALL" : "");
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        mLayoutRect.set(left, top, right, bottom);
-        if (DEBUG) log("onLayout %s %s %dx%d", changed,
-                mLayoutRect.toShortString(), mLayoutRect.width(), mLayoutRect.height());
-        super.onLayout(changed, left, top, right, bottom);
-    }
-
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        final boolean rt = super.dispatchTouchEvent(ev);
-        if (DEBUG) logTouchEvent("dispatchTouchEvent", rt, ev);
-        return rt;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        final boolean rt = super.onInterceptTouchEvent(ev);
-        if (DEBUG) logTouchEvent("onInterceptTouchEvent", rt, ev);
-        if (isApplicable()
-                && ev.getAction() == MotionEvent.ACTION_DOWN
-                && ev.getY() > mCloseButton.getBottom()
-                && mPeekable) {
-            return true;
-        }
-        return rt;
-    }
-
-    private static void logTouchEvent(String method, boolean rt, MotionEvent event) {
-        final String action = MotionEvent.actionToString(event.getAction());
-        Log.d(TAG, method + " " + (rt ? "TRUE" : "FALSE") + " " + action);
-    }
-
-    private int getExpandedBottom() {
-        int b = mModeSpinner.getMeasuredHeight() + mUntilPager.getMeasuredHeight();
-        if (mAlarmWarning.getAlpha() == 1) b += mAlarmWarning.getMeasuredHeight();
-        return b;
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        boolean rt = super.onTouchEvent(event);
-        if (DEBUG) logTouchEvent("onTouchEvent", rt, event);
-        if (!isApplicable() || !mPeekable) {
-            return rt;
-        }
-        if (event.getAction() == MotionEvent.ACTION_DOWN) {
-            mDownY = event.getY();
-            if (DEBUG) log("  mDownY=" + mDownY);
-            mDownBottom = mBottom;
-            return true;
-        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
-            final float dy = event.getY() - mDownY;
-            if (!mPopped) {
-                mPopped = true;
-                AsyncTask.execute(mPopVibration);
-            }
-            setPeeked(mDownBottom + (int)dy, getExpandedBottom(), false);
-        } else if (event.getAction() == MotionEvent.ACTION_UP
-                || event.getAction() == MotionEvent.ACTION_CANCEL) {
-            final float dy = event.getY() - mDownY;
-            setPeeked(mDownBottom + (int)dy, getExpandedBottom(), true);
-            if (mPeekable) {
-                close();
-            }
-        }
-        return rt;
-    }
-
-    private void setPeeked(int peeked, int max, boolean isDone) {
-        if (DEBUG) log("setPeeked=" + peeked);
-        final int min = mHintText.getBottom();
-        peeked = Math.max(min, Math.min(peeked, max));
-        if (!isDone && mBottom == peeked) {
-            return;
-        }
-        if (peeked == max && isDone) {
-            mPeekable = false;
-            mModeSpinner.setEnabled(true);
-            if (mAdapter != null) {
-                mAdapter.setMode(Adapter.MODE_LIMITED);
-            }
-        }
-        if (peeked == min) {
-            mPeekable = true;
-            mModeSpinner.setEnabled(false);
-        }
-        if (DEBUG) log("  mBottom=" + peeked);
-        mBottom = peeked;
-        final float f = (peeked - min) / (float)(max - min);
-        setExpanded(f);
-        requestLayout();
-    }
-
-    private void setExpanded(float f) {
-        if (DEBUG) log("setExpanded " + f);
-        final int a = (int)(Color.alpha(BACKGROUND) * f);
-        setBackgroundColor(Color.argb(a,
-                Color.red(BACKGROUND), Color.green(BACKGROUND), Color.blue(BACKGROUND)));
-        mHintText.setAlpha(1 - f);
-        mCloseButton.setAlpha(f);
-        mModeSpinner.setAlpha(f);
-        mUntilPager.setAlpha(f);
-        mSettingsButton.setAlpha(f * getSettingsButtonAlpha());
+    private int vh(View v) {
+        LayoutParams lp = (LayoutParams) v.getLayoutParams();
+        return v.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
     }
 
     private static void log(String msg, Object... args) {
@@ -406,12 +240,6 @@
         return sd;
     }
 
-    public void dispatchExternalTouchEvent(MotionEvent ev) {
-        if (isApplicable()) {
-            onTouchEvent(ev);
-        }
-    }
-
     private static void bounce(final View v, final Runnable midBounce) {
         v.animate().scaleX(BOUNCE_SCALE).scaleY(BOUNCE_SCALE).setDuration(DURATION / 3)
             .setListener(new AnimatorListenerAdapter() {
@@ -429,13 +257,18 @@
             }).start();
     }
 
-    private final Runnable mPopVibration = new Runnable() {
-        @Override
-        public void run() {
-            Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
-            v.vibrate(mPopDuration);
-        }
-    };
+    public static String modeToString(int mode) {
+        if (mode == Adapter.MODE_OFF) return "MODE_OFF";
+        if (mode == Adapter.MODE_LIMITED) return "MODE_LIMITED";
+        if (mode == Adapter.MODE_FULL) return "MODE_FULL";
+        throw new IllegalArgumentException("Invalid mode: " + mode);
+    }
+
+    public static String modeToLabel(int mode) {
+        if (mode == Adapter.MODE_LIMITED) return "Limited interruptions";
+        if (mode == Adapter.MODE_FULL) return "Zero interruptions";
+        throw new UnsupportedOperationException("Unsupported mode: " + mode);
+    }
 
     private final class UntilPager extends RelativeLayout {
         private final ImageView mPrev;
@@ -448,6 +281,7 @@
         public UntilPager(Context context, Paint pathPaint, int iconSize) {
             super(context);
             mText1 = new TextView(mContext);
+            mText1.setTextSize(TypedValue.COMPLEX_UNIT_PX, mText1.getTextSize() * 1.2f);
             mText1.setTypeface(CONDENSED);
             mText1.setTextColor(GRAY);
             mText1.setGravity(Gravity.CENTER);
@@ -456,6 +290,7 @@
             mText = mText1;
 
             mText2 = new TextView(mContext);
+            mText2.setTextSize(TypedValue.COMPLEX_UNIT_PX, mText1.getTextSize());
             mText2.setTypeface(CONDENSED);
             mText2.setTextColor(GRAY);
             mText2.setAlpha(0);
@@ -478,7 +313,7 @@
             });
 
             lp = new LayoutParams(iconSize, iconSize);
-            lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+            lp.addRule(ALIGN_PARENT_RIGHT);
             final View v2 = new View(mContext);
             v2.setBackgroundColor(BACKGROUND);
             addView(v2, lp);
@@ -532,9 +367,7 @@
         }
 
         private void setText(final TextView textView, final ExitCondition ec) {
-            SpannableStringBuilder ss = new SpannableStringBuilder(ec.line1 + "\n" + ec.line2);
-            ss.setSpan(new RelativeSizeSpan(1.5f), (ec.line1 + "\n").length(), ss.length(),
-                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+            SpannableStringBuilder ss = new SpannableStringBuilder(ec.summary);
             if (ec.action != null) {
                 ss.setSpan(new CustomLinkSpan() {
                     @Override
@@ -542,7 +375,7 @@
                         // TODO wire up links
                         Toast.makeText(mContext, ec.action, Toast.LENGTH_SHORT).show();
                     }
-                }, (ec.line1 + "\n").length(), ss.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+                }, 0, ss.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
                 textView.setMovementMethod(LinkMovementMethod.getInstance());
             } else {
                 textView.setMovementMethod(null);
@@ -558,7 +391,7 @@
         }
 
         private Path prevPath(int size) {
-            final int hp = size / 3;
+            final int hp = size * 3 / 8;
             final int vp = size / 4;
             final Path p = new Path();
             p.moveTo(size - hp, vp);
@@ -568,7 +401,7 @@
         }
 
         private Path nextPath(int size) {
-            final int hp = size / 3;
+            final int hp = size * 3 / 8;
             final int vp = size / 4;
             Path p = new Path();
             p.moveTo(hp, vp);
@@ -603,12 +436,14 @@
         public static final int MODE_LIMITED = 1;
         public static final int MODE_FULL = 2;
 
-        boolean isApplicable();
         void configure();
+        void close();
         int getMode();
         void setMode(int mode);
+        int getCommittedMode();
+        void setCommittedMode(int mode);
         void select(ExitCondition ec);
-        void cancel();
+        void init();
         void setCallbacks(Callbacks callbacks);
         ExitCondition getExitCondition(int d);
         int getExitConditionCount();
@@ -637,38 +472,38 @@
                 }
 
                 @Override
-                public View getDropDownView(int position, View convertView, ViewGroup parent) {
+                public View getDropDownView(final int position, View convertView, ViewGroup parent) {
                     if (DEBUG) log("getDropDownView %s cv=%s parent=%s",
                             position, convertView, parent);
                     final TextView tv = convertView != null ? (TextView) convertView
                             : new TextView(context);
                     final int mode = getItem(position);
-                    tv.setText(modeToString(mode));
+                    tv.setText(modeToLabel(mode));
+                    final boolean inDropdown = parent instanceof ListView;
                     if (convertView == null) {
                         if (DEBUG) log(" setting up view");
                         tv.setTextColor(GRAY);
                         tv.setTypeface(CONDENSED);
                         tv.setAllCaps(true);
-                        tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, tv.getTextSize() * 1.5f);
+                        tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, tv.getTextSize() * 1.2f);
                         final int p = (int) tv.getTextSize() / 2;
-                        if (parent instanceof ListView) {
-                            tv.setPadding(p, p, p, p);
+                        if (inDropdown) {
+                            tv.setPadding(p, p, 0, p);
                         } else {
                             tv.setGravity(Gravity.CENTER_HORIZONTAL);
-                            tv.setPadding(p, 0, p, 0);
+                            tv.setPadding(p, 0, 0, 0);
                         }
                     }
                     tv.setOnTouchListener(new OnTouchListener(){
                         @Override
                         public boolean onTouch(View v, MotionEvent event) {
-                            if (DEBUG) log("onTouch %s %s", tv.getText(),
-                                    MotionEvent.actionToString(event.getAction()));
-                            if (mAdapter != null) {
+                            if (DEBUG) log("onTouch %s %s inDropdown=%s", tv.getText(),
+                                    MotionEvent.actionToString(event.getAction()), inDropdown);
+                            if (inDropdown && mAdapter != null) {
                                 mAdapter.setMode(mode);
                             }
                             return false;
                         }
-
                     });
                     return tv;
                 }
@@ -688,16 +523,10 @@
                 if (getAdapter().getItem(i).equals(mode)) {
                     if (DEBUG) log("  setting selection = " + i);
                     setSelection(i, true);
-                    return;
+                    onDetachedFromWindow();
                 }
             }
         }
-
-        private String modeToString(int mode) {
-            if (mode == Adapter.MODE_LIMITED) return "Limited interruptions";
-            if (mode == Adapter.MODE_FULL) return "Zero interruptions";
-            throw new UnsupportedOperationException("Unsupported mode: " + mode);
-        }
     }
 
     private final class AlarmWarning extends LinearLayout {
@@ -724,29 +553,12 @@
         }
 
         public void updateState(boolean animate) {
-            final boolean visible = mAdapter != null && mAdapter.getMode() == Adapter.MODE_FULL;
-            final float alpha = visible ? 1 : 0;
+            final float alpha = isFull() ? 1 : 0;
             if (alpha == getAlpha()) {
                 return;
             }
             if (animate) {
-                final boolean in = alpha == 1;
-                animate().alpha(alpha).setUpdateListener(new AnimatorUpdateListener() {
-                    @Override
-                    public void onAnimationUpdate(ValueAnimator animation) {
-                        if (mPeekable || mClosing) {
-                            return;
-                        }
-                        float f = animation.getAnimatedFraction();
-                        if (!in) {
-                            f = 1 - f;
-                        }
-                        ZenModeView.this.mBottom = mUntilPager.getBottom()
-                                + (int)(mAlarmWarning.getMeasuredHeight() * f);
-                        if (DEBUG) log("mBottom (AW) = " + mBottom);
-                        requestLayout();
-                    }
-                }).start();
+                animate().alpha(alpha).start();
             } else {
                 setAlpha(alpha);
                 requestLayout();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.java
index 39c4faa..d2067a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeViewAdapter.java
@@ -35,23 +35,19 @@
     private final Handler mHandler = new Handler();
     private final SettingsObserver mObserver;
     private final List<ExitCondition> mExits = Arrays.asList(
-            newExit("Until you delete this", "Until", "You delete this"));
+            newExit("Until you turn this off", "Until", "You turn this off"));
 
     private Callbacks mCallbacks;
     private int mExitIndex;
-    private boolean mDeviceProvisioned;
     private int mMode;
+    private int mCommittedMode;
 
     public ZenModeViewAdapter(Context context) {
         mContext = context;
         mResolver = mContext.getContentResolver();
         mObserver = new SettingsObserver(mHandler);
         mObserver.init();
-    }
-
-    @Override
-    public boolean isApplicable() {
-        return mDeviceProvisioned;
+        init();
     }
 
     @Override
@@ -61,6 +57,18 @@
 
     @Override
     public void setMode(int mode) {
+        if (mode == mMode) return;
+        mMode = mode;
+        dispatchChanged();
+    }
+
+    @Override
+    public int getCommittedMode() {
+        return mCommittedMode;
+    }
+
+    @Override
+    public void setCommittedMode(int mode) {
         final int v = mode == MODE_LIMITED ? Settings.Global.ZEN_MODE_LIMITED
                     : mode == MODE_FULL ? Settings.Global.ZEN_MODE_FULL
                     : Settings.Global.ZEN_MODE_OFF;
@@ -74,12 +82,21 @@
     }
 
     @Override
-    public void cancel() {
+    public void init() {
         if (mExitIndex != 0) {
             mExitIndex = 0;
-            mHandler.post(mChange);
+            dispatchChanged();
         }
-        setMode(MODE_OFF);
+        final int mode = mCommittedMode == MODE_FULL ? MODE_FULL : MODE_LIMITED;
+        if (mode != mMode) {
+            mMode = mode;
+            dispatchChanged();
+        }
+    }
+
+    private void dispatchChanged() {
+        mHandler.removeCallbacks(mChanged);
+        mHandler.post(mChanged);
     }
 
     @Override
@@ -111,7 +128,7 @@
             return;
         }
         mExitIndex = i;
-        mHandler.post(mChange);
+        dispatchChanged();
     }
 
     private static ExitCondition newExit(String summary, String line1, String line2) {
@@ -122,7 +139,7 @@
         return rt;
     }
 
-    private final Runnable mChange = new Runnable() {
+    private final Runnable mChanged = new Runnable() {
         public void run() {
             if (mCallbacks == null) {
                 return;
@@ -145,24 +162,19 @@
             mResolver.registerContentObserver(
                     Settings.Global.getUriFor(Settings.Global.ZEN_MODE),
                     false, this);
-            mResolver.registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
-                    false, this);
         }
 
         @Override
         public void onChange(boolean selfChange) {
             loadSettings();
-            mChange.run();  // already on handler
+            mChanged.run();  // already on handler
         }
 
         private void loadSettings() {
-            mDeviceProvisioned = Settings.Global.getInt(mResolver,
-                    Settings.Global.DEVICE_PROVISIONED, 0) != 0;
-            mMode = getMode();
+            mCommittedMode = getModeFromSetting();
         }
 
-        private int getMode() {
+        private int getModeFromSetting() {
             final int v = Settings.Global.getInt(mResolver,
                     Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF);
             if (v == Settings.Global.ZEN_MODE_LIMITED) return MODE_LIMITED;
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 9636de7..b2cf846 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -2469,7 +2469,10 @@
             for (int stackNdx = stacks.size() - 1; stackNdx >= 0; --stackNdx) {
                 final ActivityStack stack = stacks.get(stackNdx);
                 stack.switchUserLocked(userId);
-                mWindowManager.moveTaskToTop(stack.topTask().taskId);
+                TaskRecord task = stack.topTask();
+                if (task != null) {
+                    mWindowManager.moveTaskToTop(task.taskId);
+                }
             }
         }
 
@@ -2480,7 +2483,10 @@
         final boolean homeInFront = stack.isHomeStack();
         if (stack.isOnHomeDisplay()) {
             moveHomeStack(homeInFront);
-            mWindowManager.moveTaskToTop(stack.topTask().taskId);
+            TaskRecord task = stack.topTask();
+            if (task != null) {
+                mWindowManager.moveTaskToTop(task.taskId);
+            }
         } else {
             // Stack was moved to another display while user was swapped out.
             resumeHomeActivity(null);
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 89acec9..1ff925c 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -21,14 +21,22 @@
 import android.media.session.IMediaControllerCallback;
 import android.media.session.IMediaSession;
 import android.media.session.IMediaSessionCallback;
-import android.media.RemoteControlClient;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
+import android.media.Rating;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.util.Log;
+import android.util.Slog;
 import android.view.KeyEvent;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * This is the system implementation of a Session. Apps will interact with the
@@ -37,6 +45,8 @@
 public class MediaSessionRecord implements IBinder.DeathRecipient {
     private static final String TAG = "MediaSessionImpl";
 
+    private final MessageHandler mHandler;
+
     private final int mPid;
     private final String mPackageName;
     private final String mTag;
@@ -45,13 +55,25 @@
     private final SessionCb mSessionCb;
     private final MediaSessionService mService;
 
-    private final ArrayList<IMediaControllerCallback> mSessionCallbacks =
+    private final Object mControllerLock = new Object();
+    private final ArrayList<IMediaControllerCallback> mControllerCallbacks =
             new ArrayList<IMediaControllerCallback>();
+    private final ArrayList<String> mInterfaces = new ArrayList<String>();
 
-    private int mPlaybackState = RemoteControlClient.PLAYSTATE_NONE;
+    private boolean mTransportPerformerEnabled = false;
+    private Bundle mRoute;
+
+    // TransportPerformer fields
+
+    private MediaMetadata mMetadata;
+    private PlaybackState mPlaybackState;
+    private int mRatingType;
+    // End TransportPerformer fields
+
+    private boolean mIsPublished = false;
 
     public MediaSessionRecord(int pid, String packageName, IMediaSessionCallback cb, String tag,
-            MediaSessionService service) {
+            MediaSessionService service, Handler handler) {
         mPid = pid;
         mPackageName = packageName;
         mTag = tag;
@@ -59,6 +81,7 @@
         mSession = new SessionStub();
         mSessionCb = new SessionCb(cb);
         mService = service;
+        mHandler = new MessageHandler(handler.getLooper());
     }
 
     public IMediaSession getSessionBinder() {
@@ -69,61 +92,132 @@
         return mController;
     }
 
-    public void setPlaybackStateInternal(int state) {
-        mPlaybackState = state;
-        for (int i = mSessionCallbacks.size() - 1; i >= 0; i--) {
-            IMediaControllerCallback cb = mSessionCallbacks.get(i);
-            try {
-                cb.onPlaybackUpdate(state);
-            } catch (RemoteException e) {
-                Log.d(TAG, "SessionCallback object dead in setPlaybackState.", e);
-                mSessionCallbacks.remove(i);
-            }
-        }
-    }
-
     @Override
     public void binderDied() {
         mService.sessionDied(this);
     }
 
+    public boolean isPublished() {
+        return mIsPublished;
+    }
+
     private void onDestroy() {
         mService.destroySession(this);
     }
 
+    private void pushPlaybackStateUpdate() {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onPlaybackStateChanged(mPlaybackState);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushPlaybackStateUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
+    private void pushMetadataUpdate() {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onMetadataChanged(mMetadata);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushMetadataUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
+    private void pushRouteUpdate() {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onRouteChanged(mRoute);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
+    private void pushEvent(String event, Bundle data) {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onEvent(event, data);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
     private final class SessionStub extends IMediaSession.Stub {
 
         @Override
-        public void setPlaybackState(int state) throws RemoteException {
-            setPlaybackStateInternal(state);
-        }
-
-        @Override
-        public void destroy() throws RemoteException {
+        public void destroy() {
             onDestroy();
         }
 
         @Override
-        public void sendEvent(Bundle data) throws RemoteException {
+        public void sendEvent(String event, Bundle data) {
+            mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
         }
 
         @Override
-        public IMediaController getMediaSessionToken() throws RemoteException {
+        public IMediaController getMediaController() {
             return mController;
         }
 
         @Override
-        public void setMetadata(Bundle metadata) throws RemoteException {
+        public void setRouteState(Bundle routeState) {
         }
 
         @Override
-        public void setRouteState(Bundle routeState) throws RemoteException {
+        public void setRoute(Bundle mediaRouteDescriptor) {
+            mRoute = mediaRouteDescriptor;
+            mHandler.post(MessageHandler.MSG_UPDATE_ROUTE);
         }
 
         @Override
-        public void setRoute(Bundle medaiRouteDescriptor) throws RemoteException {
+        public void publish() {
+            mIsPublished = true; // TODO push update to service
+        }
+        @Override
+        public void setTransportPerformerEnabled() {
+            mTransportPerformerEnabled = true;
         }
 
+        @Override
+        public List<String> getSupportedInterfaces() {
+            return mInterfaces;
+        }
+
+        @Override
+        public void setMetadata(MediaMetadata metadata) {
+            mMetadata = metadata;
+            mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
+        }
+
+        @Override
+        public void setPlaybackState(PlaybackState state) {
+            mPlaybackState = state;
+            mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
+        }
+
+        @Override
+        public void setRatingType(int type) {
+            mRatingType = type;
+        }
     }
 
     class SessionCb {
@@ -139,32 +233,96 @@
             try {
                 mCb.onMediaButton(mediaButtonIntent);
             } catch (RemoteException e) {
-                Log.d(TAG, "Controller object dead in sendMediaRequest.", e);
-                onDestroy();
+                Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
             }
         }
 
-        public void sendCommand(String command, Bundle extras) {
+        public void sendCommand(String command, Bundle extras, ResultReceiver cb) {
             try {
-                mCb.onCommand(command, extras);
+                mCb.onCommand(command, extras, cb);
             } catch (RemoteException e) {
-                Log.d(TAG, "Controller object dead in sendCommand.", e);
-                onDestroy();
+                Slog.e(TAG, "Remote failure in sendCommand.", e);
             }
         }
 
-        public void registerCallbackListener(IMediaSessionCallback cb) {
-
+        public void play() {
+            try {
+                mCb.onPlay();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in play.", e);
+            }
         }
 
+        public void pause() {
+            try {
+                mCb.onPause();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in pause.", e);
+            }
+        }
+
+        public void stop() {
+            try {
+                mCb.onStop();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in stop.", e);
+            }
+        }
+
+        public void next() {
+            try {
+                mCb.onNext();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in next.", e);
+            }
+        }
+
+        public void previous() {
+            try {
+                mCb.onPrevious();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in previous.", e);
+            }
+        }
+
+        public void fastForward() {
+            try {
+                mCb.onFastForward();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in fastForward.", e);
+            }
+        }
+
+        public void rewind() {
+            try {
+                mCb.onRewind();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in rewind.", e);
+            }
+        }
+
+        public void seekTo(long pos) {
+            try {
+                mCb.onSeekTo(pos);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in seekTo.", e);
+            }
+        }
+
+        public void rate(Rating rating) {
+            try {
+                mCb.onRate(rating);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in rate.", e);
+            }
+        }
     }
 
     class ControllerStub extends IMediaController.Stub {
-        /*
-         */
         @Override
-        public void sendCommand(String command, Bundle extras) throws RemoteException {
-            mSessionCb.sendCommand(command, extras);
+        public void sendCommand(String command, Bundle extras, ResultReceiver cb)
+                throws RemoteException {
+            mSessionCb.sendCommand(command, extras, cb);
         }
 
         @Override
@@ -172,29 +330,130 @@
             mSessionCb.sendMediaButton(mediaButtonIntent);
         }
 
-        /*
-         */
         @Override
-        public void registerCallbackListener(IMediaControllerCallback cb) throws RemoteException {
-            if (!mSessionCallbacks.contains(cb)) {
-                mSessionCallbacks.add(cb);
+        public void registerCallbackListener(IMediaControllerCallback cb) {
+            synchronized (mControllerLock) {
+                if (!mControllerCallbacks.contains(cb)) {
+                    mControllerCallbacks.add(cb);
+                }
             }
         }
 
-        /*
-         */
         @Override
         public void unregisterCallbackListener(IMediaControllerCallback cb)
                 throws RemoteException {
-            mSessionCallbacks.remove(cb);
+            synchronized (mControllerLock) {
+                mControllerCallbacks.remove(cb);
+            }
         }
 
-        /*
-         */
         @Override
-        public int getPlaybackState() throws RemoteException {
+        public void play() throws RemoteException {
+            mSessionCb.play();
+        }
+
+        @Override
+        public void pause() throws RemoteException {
+            mSessionCb.pause();
+        }
+
+        @Override
+        public void stop() throws RemoteException {
+            mSessionCb.stop();
+        }
+
+        @Override
+        public void next() throws RemoteException {
+            mSessionCb.next();
+        }
+
+        @Override
+        public void previous() throws RemoteException {
+            mSessionCb.previous();
+        }
+
+        @Override
+        public void fastForward() throws RemoteException {
+            mSessionCb.fastForward();
+        }
+
+        @Override
+        public void rewind() throws RemoteException {
+            mSessionCb.rewind();
+        }
+
+        @Override
+        public void seekTo(long pos) throws RemoteException {
+            mSessionCb.seekTo(pos);
+        }
+
+        @Override
+        public void rate(Rating rating) throws RemoteException {
+            mSessionCb.rate(rating);
+        }
+
+
+        @Override
+        public MediaMetadata getMetadata() {
+            return mMetadata;
+        }
+
+        @Override
+        public PlaybackState getPlaybackState() {
             return mPlaybackState;
         }
+
+        @Override
+        public int getRatingType() {
+            return mRatingType;
+        }
+
+        @Override
+        public boolean isTransportControlEnabled() throws RemoteException {
+            return mTransportPerformerEnabled;
+        }
+    }
+
+    private class MessageHandler extends Handler {
+        private static final int MSG_UPDATE_METADATA = 1;
+        private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
+        private static final int MSG_UPDATE_ROUTE = 3;
+        private static final int MSG_SEND_EVENT = 4;
+
+        public MessageHandler(Looper looper) {
+            super(looper);
+        }
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_UPDATE_METADATA:
+                    pushMetadataUpdate();
+                    break;
+                case MSG_UPDATE_PLAYBACK_STATE:
+                    pushPlaybackStateUpdate();
+                    break;
+                case MSG_UPDATE_ROUTE:
+                    pushRouteUpdate();
+                    break;
+                case MSG_SEND_EVENT:
+                    pushEvent((String) msg.obj, msg.getData());
+                    break;
+            }
+        }
+
+        public void post(int what) {
+            post(what, null);
+        }
+
+        public void post(int what, Object obj) {
+            obtainMessage(what, obj).sendToTarget();
+        }
+
+        public void post(int what, Object obj, Bundle data) {
+            Message msg = obtainMessage(what, obj);
+            msg.setData(data);
+            msg.sendToTarget();
+        }
     }
 
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index a7ff926..8fe6055 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -21,6 +21,7 @@
 import android.media.session.IMediaSessionCallback;
 import android.media.session.IMediaSessionManager;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Log;
@@ -41,6 +42,8 @@
     private final ArrayList<MediaSessionRecord> mSessions
             = new ArrayList<MediaSessionRecord>();
     private final Object mLock = new Object();
+    // TODO do we want a separate thread for handling mediasession messages?
+    private final Handler mHandler = new Handler();
 
     public MediaSessionService(Context context) {
         super(context);
@@ -91,7 +94,8 @@
 
     private MediaSessionRecord createSessionLocked(int pid, String packageName,
             IMediaSessionCallback cb, String tag) {
-        final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this);
+        final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this,
+                mHandler);
         try {
             cb.asBinder().linkToDeath(session, 0);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 243bd74..7bd88b2 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -20,11 +20,11 @@
 
 public interface NotificationDelegate {
     void onSetDisabled(int status);
-    void onClearAll();
-    void onNotificationClick(String pkg, String tag, int id);
-    void onNotificationClear(String pkg, String tag, int id);
+    void onClearAll(int userId);
+    void onNotificationClick(String pkg, String tag, int id, int userId);
+    void onNotificationClear(String pkg, String tag, int id, int userId);
     void onNotificationError(String pkg, String tag, int id,
-            int uid, int initialPid, String message);
+            int uid, int initialPid, String message, int userId);
     void onPanelRevealed();
     boolean allowDisable(int what, IBinder token, String pkg);
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index ce13a7a..f52092e 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -43,6 +43,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
@@ -56,6 +57,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.os.Vibrator;
 import android.provider.Settings;
 import android.service.notification.INotificationListener;
@@ -67,6 +69,7 @@
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.Xml;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -217,6 +220,9 @@
             ));
     private static final String EXTRA_INTERCEPT = "android.intercept";
 
+    // Users related to the current user.
+    final protected SparseArray<UserInfo> mRelatedUsers = new SparseArray<UserInfo>();
+
     private class NotificationListenerInfo implements IBinder.DeathRecipient {
         INotificationListener listener;
         ComponentName component;
@@ -910,28 +916,21 @@
         }
 
         @Override
-        public void onClearAll() {
-            // XXX to be totally correct, the caller should tell us which user
-            // this is for.
-            cancelAll(ActivityManager.getCurrentUser());
+        public void onClearAll(int userId) {
+            cancelAll(userId);
         }
 
         @Override
-        public void onNotificationClick(String pkg, String tag, int id) {
-            // XXX to be totally correct, the caller should tell us which user
-            // this is for.
+        public void onNotificationClick(String pkg, String tag, int id, int userId) {
             cancelNotification(pkg, tag, id, Notification.FLAG_AUTO_CANCEL,
-                    Notification.FLAG_FOREGROUND_SERVICE, false,
-                    ActivityManager.getCurrentUser());
+                    Notification.FLAG_FOREGROUND_SERVICE, false, userId);
         }
 
         @Override
-        public void onNotificationClear(String pkg, String tag, int id) {
-            // XXX to be totally correct, the caller should tell us which user
-            // this is for.
+        public void onNotificationClear(String pkg, String tag, int id, int userId) {
             cancelNotification(pkg, tag, id, 0,
-                Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE,
-                true, ActivityManager.getCurrentUser());
+                    Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE,
+                    true, userId);
         }
 
         @Override
@@ -969,12 +968,10 @@
 
         @Override
         public void onNotificationError(String pkg, String tag, int id,
-                int uid, int initialPid, String message) {
+                int uid, int initialPid, String message, int userId) {
             Slog.d(TAG, "onNotification error pkg=" + pkg + " tag=" + tag + " id=" + id
                     + "; will crashApplication(uid=" + uid + ", pid=" + initialPid + ")");
-            // XXX to be totally correct, the caller should tell us which user
-            // this is for.
-            cancelNotification(pkg, tag, id, 0, 0, false, UserHandle.getUserId(uid));
+            cancelNotification(pkg, tag, id, 0, 0, false, userId);
             long ident = Binder.clearCallingIdentity();
             try {
                 ActivityManagerNative.getDefault().crashApplication(uid, initialPid, pkg,
@@ -1090,6 +1087,9 @@
             } else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
                 // reload per-user settings
                 mSettingsObserver.update(null);
+                updateRelatedUserCache(context);
+            } else if (action.equals(Intent.ACTION_USER_ADDED)) {
+                updateRelatedUserCache(context);
             }
         }
     };
@@ -1223,6 +1223,7 @@
         filter.addAction(Intent.ACTION_USER_PRESENT);
         filter.addAction(Intent.ACTION_USER_STOPPED);
         filter.addAction(Intent.ACTION_USER_SWITCHED);
+        filter.addAction(Intent.ACTION_USER_ADDED);
         getContext().registerReceiver(mIntentReceiver, filter);
         IntentFilter pkgFilter = new IntentFilter();
         pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -2337,6 +2338,18 @@
     }
 
     /**
+     * Determine whether the userId applies to the notification in question, either because
+     * they match exactly, or one of them is USER_ALL (which is treated as a wildcard) or
+     * because it matches a related user.
+     */
+    private boolean notificationMatchesUserIdOrRelated(NotificationRecord r, int userId) {
+        synchronized (mRelatedUsers) {
+            return notificationMatchesUserId(r, userId)
+                    || mRelatedUsers.get(r.getUserId()) != null;
+        }
+    }
+
+    /**
      * Cancels all notifications from a given package that have all of the
      * {@code mustHaveFlags}.
      */
@@ -2424,7 +2437,7 @@
             for (int i=N-1; i>=0; i--) {
                 NotificationRecord r = mNotificationList.get(i);
 
-                if (!notificationMatchesUserId(r, userId)) {
+                if (!notificationMatchesUserIdOrRelated(r, userId)) {
                     continue;
                 }
 
@@ -2582,6 +2595,20 @@
         }
     }
 
+    private void updateRelatedUserCache(Context context) {
+        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        int currentUserId = ActivityManager.getCurrentUser();
+        if (userManager != null) {
+            List<UserInfo> relatedUsers = userManager.getRelatedUsers(currentUserId);
+            synchronized (mRelatedUsers) {
+                mRelatedUsers.clear();
+                for (UserInfo related : relatedUsers) {
+                    mRelatedUsers.put(related.id, related);
+                }
+            }
+        }
+    }
+
     private boolean isCall(String pkg, Notification n) {
         return CALL_PACKAGES.contains(pkg);
     }
diff --git a/services/core/java/com/android/server/power/DisplayPowerController.java b/services/core/java/com/android/server/power/DisplayPowerController.java
index 12d51aa..6d3702a 100644
--- a/services/core/java/com/android/server/power/DisplayPowerController.java
+++ b/services/core/java/com/android/server/power/DisplayPowerController.java
@@ -479,7 +479,6 @@
                         && mProximity == PROXIMITY_POSITIVE) {
                     mScreenOffBecauseOfProximity = true;
                     sendOnProximityPositiveWithWakelock();
-                    setScreenOn(false);
                 }
             } else if (mWaitingForNegativeProximity
                     && mScreenOffBecauseOfProximity
@@ -544,59 +543,62 @@
             mUsingScreenAutoBrightness = false;
         }
 
-        // Animate the screen on or off.
-        if (!mScreenOffBecauseOfProximity) {
-            if (mPowerRequest.wantScreenOnAny()) {
-                // Want screen on.
-                // Wait for previous off animation to complete beforehand.
-                // It is relatively short but if we cancel it and switch to the
-                // on animation immediately then the results are pretty ugly.
-                if (!mElectronBeamOffAnimator.isStarted()) {
-                    // Turn the screen on.  The contents of the screen may not yet
-                    // be visible if the electron beam has not been dismissed because
-                    // its last frame of animation is solid black.
-                    setScreenOn(true);
+        // Animate the screen on or off unless blocked.
+        if (mScreenOffBecauseOfProximity) {
+            // Screen off due to proximity.
+            setScreenOn(false);
+            unblockScreenOn();
+        } else if (mPowerRequest.wantScreenOnAny()) {
+            // Want screen on.
+            // Wait for previous off animation to complete beforehand.
+            // It is relatively short but if we cancel it and switch to the
+            // on animation immediately then the results are pretty ugly.
+            if (!mElectronBeamOffAnimator.isStarted()) {
+                // Turn the screen on.  The contents of the screen may not yet
+                // be visible if the electron beam has not been dismissed because
+                // its last frame of animation is solid black.
+                setScreenOn(true);
 
-                    if (mPowerRequest.blockScreenOn
-                            && mPowerState.getElectronBeamLevel() == 0.0f) {
-                        blockScreenOn();
-                    } else {
-                        unblockScreenOn();
-                        if (USE_ELECTRON_BEAM_ON_ANIMATION) {
-                            if (!mElectronBeamOnAnimator.isStarted()) {
-                                if (mPowerState.getElectronBeamLevel() == 1.0f) {
-                                    mPowerState.dismissElectronBeam();
-                                } else if (mPowerState.prepareElectronBeam(
-                                        mElectronBeamFadesConfig ?
-                                                ElectronBeam.MODE_FADE :
-                                                        ElectronBeam.MODE_WARM_UP)) {
-                                    mElectronBeamOnAnimator.start();
-                                } else {
-                                    mElectronBeamOnAnimator.end();
-                                }
+                if (mPowerRequest.blockScreenOn
+                        && mPowerState.getElectronBeamLevel() == 0.0f) {
+                    blockScreenOn();
+                } else {
+                    unblockScreenOn();
+                    if (USE_ELECTRON_BEAM_ON_ANIMATION) {
+                        if (!mElectronBeamOnAnimator.isStarted()) {
+                            if (mPowerState.getElectronBeamLevel() == 1.0f) {
+                                mPowerState.dismissElectronBeam();
+                            } else if (mPowerState.prepareElectronBeam(
+                                    mElectronBeamFadesConfig ?
+                                            ElectronBeam.MODE_FADE :
+                                                    ElectronBeam.MODE_WARM_UP)) {
+                                mElectronBeamOnAnimator.start();
+                            } else {
+                                mElectronBeamOnAnimator.end();
                             }
-                        } else {
-                            mPowerState.setElectronBeamLevel(1.0f);
-                            mPowerState.dismissElectronBeam();
                         }
+                    } else {
+                        mPowerState.setElectronBeamLevel(1.0f);
+                        mPowerState.dismissElectronBeam();
                     }
                 }
-            } else {
-                // Want screen off.
-                // Wait for previous on animation to complete beforehand.
-                if (!mElectronBeamOnAnimator.isStarted()) {
-                    if (!mElectronBeamOffAnimator.isStarted()) {
-                        if (mPowerState.getElectronBeamLevel() == 0.0f) {
-                            setScreenOn(false);
-                        } else if (mPowerState.prepareElectronBeam(
-                                mElectronBeamFadesConfig ?
-                                        ElectronBeam.MODE_FADE :
-                                                ElectronBeam.MODE_COOL_DOWN)
-                                && mPowerState.isScreenOn()) {
-                            mElectronBeamOffAnimator.start();
-                        } else {
-                            mElectronBeamOffAnimator.end();
-                        }
+            }
+        } else {
+            // Want screen off.
+            // Wait for previous on animation to complete beforehand.
+            unblockScreenOn();
+            if (!mElectronBeamOnAnimator.isStarted()) {
+                if (!mElectronBeamOffAnimator.isStarted()) {
+                    if (mPowerState.getElectronBeamLevel() == 0.0f) {
+                        setScreenOn(false);
+                    } else if (mPowerState.prepareElectronBeam(
+                            mElectronBeamFadesConfig ?
+                                    ElectronBeam.MODE_FADE :
+                                            ElectronBeam.MODE_COOL_DOWN)
+                            && mPowerState.isScreenOn()) {
+                        mElectronBeamOffAnimator.start();
+                    } else {
+                        mElectronBeamOffAnimator.end();
                     }
                 }
             }
@@ -641,15 +643,15 @@
     private void unblockScreenOn() {
         if (mScreenOnWasBlocked) {
             mScreenOnWasBlocked = false;
-            if (DEBUG) {
-                Slog.d(TAG, "Unblocked screen on after " +
-                        (SystemClock.elapsedRealtime() - mScreenOnBlockStartRealTime) + " ms");
+            long delay = SystemClock.elapsedRealtime() - mScreenOnBlockStartRealTime;
+            if (delay > 1000 || DEBUG) {
+                Slog.d(TAG, "Unblocked screen on after " + delay + " ms");
             }
         }
     }
 
     private void setScreenOn(boolean on) {
-        if (!mPowerState.isScreenOn() == on) {
+        if (mPowerState.isScreenOn() != on) {
             mPowerState.setScreenOn(on);
             if (on) {
                 mNotifier.onScreenOn();
diff --git a/services/core/java/com/android/server/power/DisplayPowerState.java b/services/core/java/com/android/server/power/DisplayPowerState.java
index 42af4b4..8e331ad 100644
--- a/services/core/java/com/android/server/power/DisplayPowerState.java
+++ b/services/core/java/com/android/server/power/DisplayPowerState.java
@@ -304,8 +304,15 @@
 
             int brightness = mScreenOn && mElectronBeamLevel > 0f ? mScreenBrightness : 0;
             if (mPhotonicModulator.setState(mScreenOn, brightness)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Screen ready");
+                }
                 mScreenReady = true;
                 invokeCleanListenerIfNeeded();
+            } else {
+                if (DEBUG) {
+                    Slog.d(TAG, "Screen not ready");
+                }
             }
         }
     };
@@ -355,7 +362,7 @@
                         AsyncTask.THREAD_POOL_EXECUTOR.execute(mTask);
                     }
                 }
-                return mChangeInProgress;
+                return !mChangeInProgress;
             }
         }
 
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 40ebe8d..fab972f 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -1047,6 +1047,9 @@
         if (!mSystemReady || mDirty == 0) {
             return;
         }
+        if (!Thread.holdsLock(mLock)) {
+            Slog.wtf(TAG, "Power manager lock was not held when calling updatePowerStateLocked");
+        }
 
         // Phase 0: Basic state updates.
         updateIsPoweredLocked(mDirty);
@@ -1813,7 +1816,12 @@
 
     private boolean isScreenOnInternal() {
         synchronized (mLock) {
-            return isScreenOnLocked();
+            // XXX This is a temporary hack to let certain parts of the system pretend the
+            // screen is still on even when dozing and we would normally want to report
+            // screen off.  Will be removed when the window manager is modified to use
+            // the true blanking state of the display.
+            return isScreenOnLocked()
+                    || mWakefulness == WAKEFULNESS_DOZING;
         }
     }
 
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 8219eb5..1568d6f 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -535,11 +535,11 @@
     }
 
     @Override
-    public void onNotificationClick(String pkg, String tag, int id) {
+    public void onNotificationClick(String pkg, String tag, int id, int userId) {
         enforceStatusBarService();
         long identity = Binder.clearCallingIdentity();
         try {
-            mNotificationDelegate.onNotificationClick(pkg, tag, id);
+            mNotificationDelegate.onNotificationClick(pkg, tag, id, userId);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -547,34 +547,35 @@
 
     @Override
     public void onNotificationError(String pkg, String tag, int id,
-            int uid, int initialPid, String message) {
+            int uid, int initialPid, String message, int userId) {
         enforceStatusBarService();
         long identity = Binder.clearCallingIdentity();
         try {
             // WARNING: this will call back into us to do the remove.  Don't hold any locks.
-            mNotificationDelegate.onNotificationError(pkg, tag, id, uid, initialPid, message);
+            mNotificationDelegate.onNotificationError(pkg, tag, id, uid, initialPid, message,
+                    userId);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
     }
 
     @Override
-    public void onNotificationClear(String pkg, String tag, int id) {
+    public void onNotificationClear(String pkg, String tag, int id, int userId) {
         enforceStatusBarService();
         long identity = Binder.clearCallingIdentity();
         try {
-            mNotificationDelegate.onNotificationClear(pkg, tag, id);
+            mNotificationDelegate.onNotificationClear(pkg, tag, id, userId);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
     }
 
     @Override
-    public void onClearAllNotifications() {
+    public void onClearAllNotifications(int userId) {
         enforceStatusBarService();
         long identity = Binder.clearCallingIdentity();
         try {
-            mNotificationDelegate.onClearAll();
+            mNotificationDelegate.onClearAll(userId);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
index 7ff81e4..3114ca9 100644
--- a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
+++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
@@ -1,7 +1,24 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 
 import android.app.Activity;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.Menu;
@@ -79,10 +96,10 @@
             switch (v.getId()) {
                 case R.id.play_button:
                     Log.d(TAG, "Play button pressed, in state " + mPlaybackState);
-                    if (mPlaybackState == Renderer.STATE_PAUSED
-                            || mPlaybackState == Renderer.STATE_ENDED) {
+                    if (mPlaybackState == PlaybackState.PLAYSTATE_PAUSED
+                            || mPlaybackState == PlaybackState.PLAYSTATE_STOPPED) {
                         mPlayer.play();
-                    } else if (mPlaybackState == Renderer.STATE_PLAYING) {
+                    } else if (mPlaybackState == PlaybackState.PLAYSTATE_PLAYING) {
                         mPlayer.pause();
                     }
                     break;
@@ -97,48 +114,55 @@
 
     private PlayerController.Listener mListener = new PlayerController.Listener() {
         @Override
-        public void onSessionStateChange(int state) {
-            mPlaybackState = state;
+        public void onPlaybackStateChange(PlaybackState state) {
+            mPlaybackState = state.getState();
             boolean enablePlay = false;
+            StringBuilder statusBuilder = new StringBuilder();
             switch (mPlaybackState) {
-                case Renderer.STATE_PLAYING:
-                    mStatusView.setText("playing");
+                case PlaybackState.PLAYSTATE_PLAYING:
+                    statusBuilder.append("playing");
                     mPlayButton.setText("Pause");
                     enablePlay = true;
                     break;
-                case Renderer.STATE_PAUSED:
-                    mStatusView.setText("paused");
+                case PlaybackState.PLAYSTATE_PAUSED:
+                    statusBuilder.append("paused");
                     mPlayButton.setText("Play");
                     enablePlay = true;
                     break;
-                case Renderer.STATE_ENDED:
-                    mStatusView.setText("ended");
+                case PlaybackState.PLAYSTATE_STOPPED:
+                    statusBuilder.append("ended");
                     mPlayButton.setText("Play");
                     enablePlay = true;
                     break;
-                case Renderer.STATE_ERROR:
-                    mStatusView.setText("error");
+                case PlaybackState.PLAYSTATE_ERROR:
+                    statusBuilder.append("error: ").append(state.getErrorMessage());
                     break;
-                case Renderer.STATE_PREPARING:
-                    mStatusView.setText("preparing");
+                case PlaybackState.PLAYSTATE_BUFFERING:
+                    statusBuilder.append("buffering");
                     break;
-                case Renderer.STATE_READY:
-                    mStatusView.setText("ready");
+                case PlaybackState.PLAYSTATE_NONE:
+                    statusBuilder.append("none");
                     break;
-                case Renderer.STATE_STOPPED:
-                    mStatusView.setText("stopped");
-                    break;
+                default:
+                    statusBuilder.append(mPlaybackState);
             }
+            statusBuilder.append(" -- At position: ").append(state.getPosition());
+            mStatusView.setText(statusBuilder.toString());
             mPlayButton.setEnabled(enablePlay);
         }
 
         @Override
-        public void onPlayerStateChange(int state) {
+        public void onConnectionStateChange(int state) {
             if (state == PlayerController.STATE_DISCONNECTED) {
                 setControlsEnabled(false);
             } else if (state == PlayerController.STATE_CONNECTED) {
                 setControlsEnabled(true);
             }
         }
+
+        @Override
+        public void onMetadataChange(MediaMetadata metadata) {
+            Log.d(TAG, "Metadata update! Title: " + metadata);
+        }
     };
 }
diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
index 01610cd..573f7ff 100644
--- a/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
+++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.content.Context;
@@ -5,9 +20,6 @@
 
 import java.util.ArrayList;
 
-/**
- * TODO: Insert description here. (generated by epastern)
- */
 public class OnePlayerService extends PlayerService {
     private static final String TAG = "OnePlayerService";
 
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerController.java b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
index 3f15db5..e831ec6 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerController.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
@@ -1,8 +1,27 @@
 
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.media.session.MediaController;
+import android.media.session.MediaMetadata;
 import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.media.session.TransportController;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -11,22 +30,23 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.util.Log;
-import android.view.KeyEvent;
 
 import com.android.onemedia.playback.RequestUtils;
 
 public class PlayerController {
-    private static final String TAG = "PlayerSession";
+    private static final String TAG = "PlayerController";
 
     public static final int STATE_DISCONNECTED = 0;
     public static final int STATE_CONNECTED = 1;
 
     protected MediaController mController;
     protected IPlayerService mBinder;
+    protected TransportController mTransportControls;
 
     private final Intent mServiceIntent;
     private Context mContext;
     private Listener mListener;
+    private TransportListener mTransportListener = new TransportListener();
     private SessionCallback mControllerCb;
     private MediaSessionManager mManager;
     private Handler mHandler = new Handler();
@@ -52,7 +72,7 @@
         Log.d(TAG, "Listener set to " + listener + " session is " + mController);
         if (mListener != null) {
             mHandler = new Handler();
-            mListener.onPlayerStateChange(
+            mListener.onConnectionStateChange(
                     mController == null ? STATE_DISCONNECTED : STATE_CONNECTED);
         }
     }
@@ -70,11 +90,15 @@
     }
 
     public void play() {
-        mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PLAY);
+        if (mTransportControls != null) {
+            mTransportControls.play();
+        }
     }
 
     public void pause() {
-        mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PAUSE);
+        if (mTransportControls != null) {
+            mTransportControls.pause();
+        }
     }
 
     public void setContent(String source) {
@@ -113,10 +137,11 @@
             }
             mBinder = null;
             mController = null;
+            mTransportControls = null;
             Log.d(TAG, "Disconnected from PlayerService");
 
             if (mListener != null) {
-                mListener.onPlayerStateChange(STATE_DISCONNECTED);
+                mListener.onConnectionStateChange(STATE_DISCONNECTED);
             }
         }
 
@@ -125,33 +150,60 @@
             mBinder = IPlayerService.Stub.asInterface(service);
             Log.d(TAG, "service is " + service + " binder is " + mBinder);
             try {
-                mController = new MediaController(mBinder.getSessionToken());
+                mController = MediaController.fromToken(mBinder.getSessionToken());
             } catch (RemoteException e) {
                 Log.e(TAG, "Error getting session", e);
                 return;
             }
             mController.addCallback(mControllerCb, mHandler);
+            mTransportControls = mController.getTransportController();
+            if (mTransportControls != null) {
+                mTransportControls.addStateListener(mTransportListener);
+            }
             Log.d(TAG, "Ready to use PlayerService");
 
             if (mListener != null) {
-                mListener.onPlayerStateChange(STATE_CONNECTED);
+                mListener.onConnectionStateChange(STATE_CONNECTED);
+                if (mTransportControls != null) {
+                    mListener.onPlaybackStateChange(mTransportControls.getPlaybackState());
+                }
             }
         }
     };
 
     private class SessionCallback extends MediaController.Callback {
         @Override
-        public void onPlaybackStateChange(int state) {
-            if (mListener != null) {
-                mListener.onSessionStateChange(state);
+        public void onRouteChanged(Bundle route) {
+            // TODO
+        }
+    }
+
+    private class TransportListener extends TransportController.TransportStateListener {
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            if (state == null) {
+                return;
             }
+            Log.d(TAG, "Received playback state change to state " + state.getState());
+            if (mListener != null) {
+                mListener.onPlaybackStateChange(state);
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            if (metadata == null) {
+                return;
+            }
+            Log.d(TAG, "Received metadata change, title is "
+                    + metadata.getString(MediaMetadata.METADATA_KEY_TITLE));
         }
     }
 
     public interface Listener {
-        public void onSessionStateChange(int state);
-
-        public void onPlayerStateChange(int state);
+        public void onPlaybackStateChange(PlaybackState state);
+        public void onMetadataChange(MediaMetadata metadata);
+        public void onConnectionStateChange(int state);
     }
 
 }
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerService.java b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
index 0b2ba8f..0ad6dd1 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerService.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
@@ -1,11 +1,28 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.app.Service;
 import android.content.Intent;
 import android.media.session.MediaSessionToken;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.Log;
 
 import com.android.onemedia.playback.IRequestCallback;
 import com.android.onemedia.playback.RequestUtils;
@@ -18,14 +35,19 @@
     private PlayerBinder mBinder;
     private PlayerSession mSession;
     private Intent mIntent;
+    private boolean mStarted = false;
 
     private ArrayList<IPlayerCallback> mCbs = new ArrayList<IPlayerCallback>();
 
     @Override
     public void onCreate() {
+        Log.d(TAG, "onCreate");
         mIntent = onCreateServiceIntent();
-        mSession = onCreatePlayerController();
-        mSession.createSession();
+        if (mSession == null) {
+            mSession = onCreatePlayerController();
+            mSession.createSession();
+            mSession.setListener(mPlayerListener);
+        }
     }
 
     @Override
@@ -38,12 +60,31 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.d(TAG, "onStartCommand");
         return START_STICKY;
     }
 
     @Override
     public void onDestroy() {
+        Log.d(TAG, "onDestroy");
         mSession.onDestroy();
+        mSession = null;
+    }
+
+    public void onPlaybackStarted() {
+        if (!mStarted) {
+            Log.d(TAG, "Starting self");
+            startService(onCreateServiceIntent());
+            mStarted = true;
+        }
+    }
+
+    public void onPlaybackEnded() {
+        if (mStarted) {
+            Log.d(TAG, "Stopping self");
+            stopSelf();
+            mStarted = false;
+        }
     }
 
     protected Intent onCreateServiceIntent() {
@@ -58,6 +99,21 @@
         return null;
     }
 
+    private final PlayerSession.Listener mPlayerListener = new PlayerSession.Listener() {
+        @Override
+        public void onPlayStateChanged(PlaybackState state) {
+            switch (state.getState()) {
+                case PlaybackState.PLAYSTATE_PLAYING:
+                    onPlaybackStarted();
+                    break;
+                case PlaybackState.PLAYSTATE_STOPPED:
+                case PlaybackState.PLAYSTATE_ERROR:
+                    onPlaybackEnded();
+                    break;
+            }
+        }
+    };
+
     public class PlayerBinder extends IPlayerService.Stub {
         @Override
         public void sendRequest(String action, Bundle params, IRequestCallback cb) {
@@ -94,7 +150,6 @@
 
         @Override
         public MediaSessionToken getSessionToken() throws RemoteException {
-            // TODO(epastern): Auto-generated method stub
             return mSession.getSessionToken();
         }
     }
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
index e5fb0d0..a2d7897e 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.content.Context;
@@ -5,6 +20,8 @@
 import android.media.session.MediaSession;
 import android.media.session.MediaSessionManager;
 import android.media.session.MediaSessionToken;
+import android.media.session.PlaybackState;
+import android.media.session.TransportPerformer;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -14,14 +31,18 @@
 import com.android.onemedia.playback.RendererFactory;
 
 public class PlayerSession {
-    private static final String TAG = "PlayerController";
+    private static final String TAG = "PlayerSession";
 
     protected MediaSession mSession;
     protected Context mContext;
     protected RendererFactory mRendererFactory;
     protected LocalRenderer mRenderer;
-    protected ControllerCb mCallback;
-    protected RenderListener mRenderListener;
+    protected MediaSession.Callback mCallback;
+    protected Renderer.Listener mRenderListener;
+    protected TransportPerformer mPerformer;
+
+    protected PlaybackState mPlaybackState;
+    protected Listener mListener;
 
     public PlayerSession(Context context) {
         mContext = context;
@@ -29,6 +50,9 @@
         mRenderer = new LocalRenderer(context, null);
         mCallback = new ControllerCb();
         mRenderListener = new RenderListener();
+        mPlaybackState = new PlaybackState();
+        mPlaybackState.setActions(PlaybackState.ACTION_PAUSE
+                | PlaybackState.ACTION_PLAY);
 
         mRenderer.registerListener(mRenderListener);
     }
@@ -42,6 +66,10 @@
         Log.d(TAG, "Creating session for package " + mContext.getBasePackageName());
         mSession = man.createSession("OneMedia");
         mSession.addCallback(mCallback);
+        mPerformer = mSession.setTransportPerformerEnabled();
+        mPerformer.addListener(new TransportListener());
+        mPerformer.setPlaybackState(mPlaybackState);
+        mSession.publish();
     }
 
     public void onDestroy() {
@@ -54,6 +82,10 @@
         }
     }
 
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
     public MediaSessionToken getSessionToken() {
         return mSession.getSessionToken();
     }
@@ -66,16 +98,58 @@
         mRenderer.setNextContent(request);
     }
 
-    protected class RenderListener implements Renderer.Listener {
+    public interface Listener {
+        public void onPlayStateChanged(PlaybackState state);
+    }
+
+    private class RenderListener implements Renderer.Listener {
 
         @Override
         public void onError(int type, int extra, Bundle extras, Throwable error) {
-            mSession.setPlaybackState(Renderer.STATE_ERROR);
+            Log.d(TAG, "Sending onError with type " + type + " and extra " + extra);
+            mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+            if (error != null) {
+                mPlaybackState.setErrorMessage(error.getLocalizedMessage());
+            }
+            mPerformer.setPlaybackState(mPlaybackState);
+            if (mListener != null) {
+                mListener.onPlayStateChanged(mPlaybackState);
+            }
         }
 
         @Override
         public void onStateChanged(int newState) {
-            mSession.setPlaybackState(newState);
+            if (newState != Renderer.STATE_ERROR) {
+                mPlaybackState.setErrorMessage(null);
+            }
+            switch (newState) {
+                case Renderer.STATE_ENDED:
+                case Renderer.STATE_STOPPED:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED);
+                    break;
+                case Renderer.STATE_INIT:
+                case Renderer.STATE_PREPARING:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING);
+                    break;
+                case Renderer.STATE_ERROR:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    break;
+                case Renderer.STATE_PAUSED:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+                    break;
+                case Renderer.STATE_PLAYING:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING);
+                    break;
+                default:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    mPlaybackState.setErrorMessage("unkown state");
+                    break;
+            }
+            mPlaybackState.setPosition(mRenderer.getSeekPosition());
+            mPerformer.setPlaybackState(mPlaybackState);
+            if (mListener != null) {
+                mListener.onPlayStateChanged(mPlaybackState);
+            }
         }
 
         @Override
@@ -84,7 +158,13 @@
 
         @Override
         public void onFocusLost() {
-            mSession.setPlaybackState(Renderer.STATE_PAUSED);
+            Log.d(TAG, "Focus lost, changing state to " + Renderer.STATE_PAUSED);
+            mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+            mPlaybackState.setPosition(mRenderer.getSeekPosition());
+            mPerformer.setPlaybackState(mPlaybackState);
+            if (mListener != null) {
+                mListener.onPlayStateChanged(mPlaybackState);
+            }
         }
 
         @Override
@@ -93,7 +173,7 @@
 
     }
 
-    protected class ControllerCb extends MediaSession.Callback {
+    private class ControllerCb extends MediaSession.Callback {
 
         @Override
         public void onMediaButton(Intent mediaRequestIntent) {
@@ -114,4 +194,16 @@
         }
     }
 
+    private class TransportListener extends TransportPerformer.Listener {
+        @Override
+        public void onPlay() {
+            mRenderer.onPlay();
+        }
+
+        @Override
+        public void onPause() {
+            mRenderer.onPause();
+        }
+    }
+
 }
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
index 7493366..7f62f66 100644
--- a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
+++ b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
@@ -499,11 +499,12 @@
     @Override
     public boolean onPause() {
         MediaPlayer player = mPlayer;
+        // If the user paused us make sure we won't start playing again until
+        // asked to
+        mPlayOnReady = false;
         if (player != null && (mState & CAN_PAUSE) != 0) {
             player.pause();
             setState(STATE_PAUSED);
-        } else if ((mState & CAN_READY_PLAY) != 0) {
-            mPlayOnReady = false;
         } else if (!isPaused()) {
             return false;
         }