Merge "Revert "Destroy docked divider surface when it's hidden.""
diff --git a/core/java/android/os/UserManagerInternal.java b/core/java/android/os/UserManagerInternal.java
index 26c7a1e..898b6cf 100644
--- a/core/java/android/os/UserManagerInternal.java
+++ b/core/java/android/os/UserManagerInternal.java
@@ -69,4 +69,16 @@
 
     /** Remove a {@link UserRestrictionsListener}. */
     public abstract void removeUserRestrictionsListener(UserRestrictionsListener listener);
+
+    /**
+     * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to update
+     * whether the device is managed by device owner.
+     */
+    public abstract void setDeviceManaged(boolean isManaged);
+
+    /**
+     * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to update
+     * whether the user is managed by profile owner.
+     */
+    public abstract void setUserManaged(int userId, boolean isManaged);
 }
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 4b63c36..1d4d572 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -413,6 +413,14 @@
         public static final String POST_DIAL_DIGITS = "post_dial_digits";
 
         /**
+         * Indicates that the entry will be copied from primary user to other users.
+         * <P>Type: INTEGER</P>
+         *
+         * @hide
+         */
+        public static final String ADD_FOR_ALL_USERS = "add_for_all_users";
+
+        /**
          * If a successful call is made that is longer than this duration, update the phone number
          * in the ContactsProvider with the normalized version of the number, based on the user's
          * current country code.
@@ -444,7 +452,7 @@
                 int presentation, int callType, int features, PhoneAccountHandle accountHandle,
                 long start, int duration, Long dataUsage) {
             return addCall(ci, context, number, "", presentation, callType, features, accountHandle,
-                    start, duration, dataUsage, false, false);
+                    start, duration, dataUsage, false, null, false);
         }
 
 
@@ -467,7 +475,9 @@
          *                  the call.
          * @param addForAllUsers If true, the call is added to the call log of all currently
          *        running users. The caller must have the MANAGE_USERS permission if this is true.
-         *
+         * @param userToBeInsertedTo {@link UserHandle} of user that the call is going to be
+         *                           inserted to. null if it is inserted to the current user. The
+         *                           value is ignored if @{link addForAllUsers} is true.
          * @result The URI of the call log entry belonging to the user that made or received this
          *        call.
          * {@hide}
@@ -475,9 +485,10 @@
         public static Uri addCall(CallerInfo ci, Context context, String number,
                 String postDialDigits, int presentation, int callType, int features,
                 PhoneAccountHandle accountHandle, long start, int duration, Long dataUsage,
-                boolean addForAllUsers) {
+                boolean addForAllUsers, UserHandle userToBeInsertedTo) {
             return addCall(ci, context, number, postDialDigits, presentation, callType, features,
-                    accountHandle, start, duration, dataUsage, addForAllUsers, false);
+                    accountHandle, start, duration, dataUsage, addForAllUsers, userToBeInsertedTo,
+                    false);
         }
 
         /**
@@ -501,6 +512,9 @@
          *                  the call.
          * @param addForAllUsers If true, the call is added to the call log of all currently
          *        running users. The caller must have the MANAGE_USERS permission if this is true.
+         * @param userToBeInsertedTo {@link UserHandle} of user that the call is going to be
+         *                           inserted to. null if it is inserted to the current user. The
+         *                           value is ignored if @{link addForAllUsers} is true.
          * @param is_read Flag to show if the missed call log has been read by the user or not.
          *                Used for call log restore of missed calls.
          *
@@ -511,7 +525,7 @@
         public static Uri addCall(CallerInfo ci, Context context, String number,
                 String postDialDigits, int presentation, int callType, int features,
                 PhoneAccountHandle accountHandle, long start, int duration, Long dataUsage,
-                boolean addForAllUsers, boolean is_read) {
+                boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean is_read) {
             final ContentResolver resolver = context.getContentResolver();
             int numberPresentation = PRESENTATION_ALLOWED;
 
@@ -575,6 +589,7 @@
             values.put(PHONE_ACCOUNT_ID, accountId);
             values.put(PHONE_ACCOUNT_ADDRESS, accountAddress);
             values.put(NEW, Integer.valueOf(1));
+            values.put(ADD_FOR_ALL_USERS, addForAllUsers ? 1 : 0);
 
             if (callType == MISSED_TYPE) {
                 values.put(IS_READ, Integer.valueOf(is_read ? 1 : 0));
@@ -650,9 +665,13 @@
                     }
                 }
             } else {
-                result = addEntryAndRemoveExpiredEntries(context, CONTENT_URI, values);
+                Uri uri = CONTENT_URI;
+                if (userToBeInsertedTo != null) {
+                    uri = ContentProvider
+                            .maybeAddUserId(CONTENT_URI, userToBeInsertedTo.getIdentifier());
+                }
+                result = addEntryAndRemoveExpiredEntries(context, uri, values);
             }
-
             return result;
         }
 
diff --git a/core/java/android/security/net/config/NetworkSecurityTrustManager.java b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
index 7f5b3ca..2b860fa 100644
--- a/core/java/android/security/net/config/NetworkSecurityTrustManager.java
+++ b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
@@ -65,7 +65,7 @@
     @Override
     public void checkClientTrusted(X509Certificate[] chain, String authType)
             throws CertificateException {
-        throw new CertificateException("Client authentication not supported");
+        mDelegate.checkClientTrusted(chain, authType);
     }
 
     @Override
@@ -149,6 +149,6 @@
 
     @Override
     public X509Certificate[] getAcceptedIssuers() {
-        return new X509Certificate[0];
+        return mDelegate.getAcceptedIssuers();
     }
 }
diff --git a/core/java/android/security/net/config/RootTrustManager.java b/core/java/android/security/net/config/RootTrustManager.java
index b87bf1f..e307ad0 100644
--- a/core/java/android/security/net/config/RootTrustManager.java
+++ b/core/java/android/security/net/config/RootTrustManager.java
@@ -35,7 +35,6 @@
  * @hide */
 public class RootTrustManager implements X509TrustManager {
     private final ApplicationConfig mConfig;
-    private static final X509Certificate[] EMPTY_ISSUERS = new X509Certificate[0];
 
     public RootTrustManager(ApplicationConfig config) {
         if (config == null) {
@@ -47,7 +46,10 @@
     @Override
     public void checkClientTrusted(X509Certificate[] chain, String authType)
             throws CertificateException {
-        throw new CertificateException("Client authentication not supported");
+        // Use the default configuration for all client authentication. Domain specific configs are
+        // only for use in checking server trust not client trust.
+        NetworkSecurityConfig config = mConfig.getConfigForHostname("");
+        config.getTrustManager().checkClientTrusted(chain, authType);
     }
 
     @Override
@@ -84,6 +86,10 @@
 
     @Override
     public X509Certificate[] getAcceptedIssuers() {
-        return EMPTY_ISSUERS;
+        // getAcceptedIssuers is meant to be used to determine which trust anchors the server will
+        // accept when verifying clients. Domain specific configs are only for use in checking
+        // server trust not client trust so use the default config.
+        NetworkSecurityConfig config = mConfig.getConfigForHostname("");
+        return config.getTrustManager().getAcceptedIssuers();
     }
 }
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 19a98f3..329d1b0 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -436,7 +436,8 @@
                         mCurId = res.id;
                         mBindSequence = res.sequence;
                     }
-                    startInputInner(null, 0, 0, 0);
+                    startInputInner(InputMethodClient.START_INPUT_REASON_BOUND_TO_IMMS,
+                            null, 0, 0, 0);
                     return;
                 }
                 case MSG_UNBIND: {
@@ -461,7 +462,9 @@
                         startInput = mActive;
                     }
                     if (startInput) {
-                        startInputInner(null, 0, 0, 0);
+                        startInputInner(
+                                InputMethodClient.START_INPUT_REASON_UNBOUND_FROM_IMMS, null, 0, 0,
+                                0);
                     }
                     return;
                 }
@@ -494,7 +497,10 @@
                             // In that case, we really should not call
                             // mServedInputConnection.finishComposingText.
                             if (checkFocusNoStartInput(mHasBeenInactive, false)) {
-                                startInputInner(null, 0, 0, 0);
+                                final int reason = active ?
+                                        InputMethodClient.START_INPUT_REASON_ACTIVATED_BY_IMMS :
+                                        InputMethodClient.START_INPUT_REASON_DEACTIVATED_BY_IMMS;
+                                startInputInner(reason, null, 0, 0, 0);
                             }
                         }
                     }
@@ -1118,18 +1124,23 @@
             
             mServedConnecting = true;
         }
-        
-        startInputInner(null, 0, 0, 0);
+
+        startInputInner(InputMethodClient.START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API, null, 0,
+                0, 0);
     }
-    
-    boolean startInputInner(IBinder windowGainingFocus, int controlFlags, int softInputMode,
+
+    boolean startInputInner(@InputMethodClient.StartInputReason final int startInputReason,
+            IBinder windowGainingFocus, int controlFlags, int softInputMode,
             int windowFlags) {
         final View view;
         synchronized (mH) {
             view = mServedView;
             
             // Make sure we have a window token for the served view.
-            if (DEBUG) Log.v(TAG, "Starting input: view=" + view);
+            if (DEBUG) {
+                Log.v(TAG, "Starting input: view=" + view +
+                        " reason=" + InputMethodClient.getStartInputReason(startInputReason));
+            }
             if (view == null) {
                 if (DEBUG) Log.v(TAG, "ABORT input: no served view!");
                 return false;
@@ -1157,7 +1168,7 @@
             vh.post(new Runnable() {
                 @Override
                 public void run() {
-                    startInputInner(null, 0, 0, 0);
+                    startInputInner(startInputReason, null, 0, 0, 0);
                 }
             });
             return false;
@@ -1221,11 +1232,11 @@
                         + Integer.toHexString(controlFlags));
                 InputBindResult res;
                 if (windowGainingFocus != null) {
-                    res = mService.windowGainedFocus(mClient, windowGainingFocus,
+                    res = mService.windowGainedFocus(startInputReason, mClient, windowGainingFocus,
                             controlFlags, softInputMode, windowFlags,
                             tba, servedContext);
                 } else {
-                    res = mService.startInput(mClient,
+                    res = mService.startInput(startInputReason, mClient,
                             servedContext, tba, controlFlags);
                 }
                 if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
@@ -1352,7 +1363,7 @@
      */
     public void checkFocus() {
         if (checkFocusNoStartInput(false, true)) {
-            startInputInner(null, 0, 0, 0);
+            startInputInner(InputMethodClient.START_INPUT_REASON_CHECK_FOCUS, null, 0, 0, 0);
         }
     }
 
@@ -1440,8 +1451,8 @@
             // should be done in conjunction with telling the system service
             // about the window gaining focus, to help make the transition
             // smooth.
-            if (startInputInner(rootView.getWindowToken(),
-                    controlFlags, softInputMode, windowFlags)) {
+            if (startInputInner(InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN,
+                    rootView.getWindowToken(), controlFlags, softInputMode, windowFlags)) {
                 return;
             }
         }
@@ -1451,8 +1462,10 @@
         synchronized (mH) {
             try {
                 if (DEBUG) Log.v(TAG, "Reporting focus gain, without startInput");
-                mService.windowGainedFocus(mClient, rootView.getWindowToken(),
-                        controlFlags, softInputMode, windowFlags, null, null);
+                mService.windowGainedFocus(
+                        InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
+                        rootView.getWindowToken(), controlFlags, softInputMode, windowFlags, null,
+                        null);
             } catch (RemoteException e) {
             }
         }
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 60c5e42..db3ecc6 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -45,9 +45,10 @@
     void addClient(in IInputMethodClient client,
             in IInputContext inputContext, int uid, int pid);
     void removeClient(in IInputMethodClient client);
-            
-    InputBindResult startInput(in IInputMethodClient client,
-            IInputContext inputContext, in EditorInfo attribute, int controlFlags);
+
+    InputBindResult startInput(/* @InputMethodClient.StartInputReason */ int startInputReason,
+            in IInputMethodClient client, IInputContext inputContext, in EditorInfo attribute,
+            int controlFlags);
     void finishInput(in IInputMethodClient client);
     boolean showSoftInput(in IInputMethodClient client, int flags,
             in ResultReceiver resultReceiver);
@@ -55,9 +56,11 @@
             in ResultReceiver resultReceiver);
     // Report that a window has gained focus.  If 'attribute' is non-null,
     // this will also do a startInput.
-    InputBindResult windowGainedFocus(in IInputMethodClient client, in IBinder windowToken,
-            int controlFlags, int softInputMode, int windowFlags,
-            in EditorInfo attribute, IInputContext inputContext);
+    InputBindResult windowGainedFocus(
+            /* @InputMethodClient.StartInputReason */ int startInputReason,
+            in IInputMethodClient client, in IBinder windowToken, int controlFlags,
+            int softInputMode, int windowFlags, in EditorInfo attribute,
+            IInputContext inputContext);
 
     void showInputMethodPickerFromClient(in IInputMethodClient client,
             int auxiliarySubtypeMode);
diff --git a/core/java/com/android/internal/view/InputMethodClient.java b/core/java/com/android/internal/view/InputMethodClient.java
index a035343..c27e9aa 100644
--- a/core/java/com/android/internal/view/InputMethodClient.java
+++ b/core/java/com/android/internal/view/InputMethodClient.java
@@ -23,6 +23,49 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 public final class InputMethodClient {
+    public static final int START_INPUT_REASON_UNSPECIFIED = 0;
+    public static final int START_INPUT_REASON_WINDOW_FOCUS_GAIN = 1;
+    public static final int START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY = 2;
+    public static final int START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API = 3;
+    public static final int START_INPUT_REASON_CHECK_FOCUS = 4;
+    public static final int START_INPUT_REASON_BOUND_TO_IMMS = 5;
+    public static final int START_INPUT_REASON_UNBOUND_FROM_IMMS = 6;
+    public static final int START_INPUT_REASON_ACTIVATED_BY_IMMS = 7;
+    public static final int START_INPUT_REASON_DEACTIVATED_BY_IMMS = 8;
+
+    @Retention(SOURCE)
+    @IntDef({START_INPUT_REASON_UNSPECIFIED, START_INPUT_REASON_WINDOW_FOCUS_GAIN,
+            START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY,
+            START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API, START_INPUT_REASON_CHECK_FOCUS,
+            START_INPUT_REASON_BOUND_TO_IMMS, START_INPUT_REASON_ACTIVATED_BY_IMMS,
+            START_INPUT_REASON_DEACTIVATED_BY_IMMS})
+    public @interface StartInputReason {}
+
+    public static String getStartInputReason(@StartInputReason final int reason) {
+        switch (reason) {
+            case START_INPUT_REASON_UNSPECIFIED:
+                return "UNSPECIFIED";
+            case START_INPUT_REASON_WINDOW_FOCUS_GAIN:
+                return "WINDOW_FOCUS_GAIN";
+            case START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY:
+                return "WINDOW_FOCUS_GAIN_REPORT_ONLY";
+            case START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API:
+                return "APP_CALLED_RESTART_INPUT_API";
+            case START_INPUT_REASON_CHECK_FOCUS:
+                return "CHECK_FOCUS";
+            case START_INPUT_REASON_BOUND_TO_IMMS:
+                return "BOUND_TO_IMMS";
+            case START_INPUT_REASON_UNBOUND_FROM_IMMS:
+                return "UNBOUND_FROM_IMMS";
+            case START_INPUT_REASON_ACTIVATED_BY_IMMS:
+                return "ACTIVATED_BY_IMMS";
+            case START_INPUT_REASON_DEACTIVATED_BY_IMMS:
+                return "DEACTIVATED_BY_IMMS";
+            default:
+                return "Unknown=" + reason;
+        }
+    }
+
     public static final int UNBIND_REASON_UNSPECIFIED = 0;
     public static final int UNBIND_REASON_SWITCH_CLIENT = 1;
     public static final int UNBIND_REASON_SWITCH_IME = 2;
diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java b/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
index 042dc83..fdebf37 100644
--- a/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
+++ b/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
@@ -28,6 +28,7 @@
 
 import libcore.util.EmptyArray;
 
+import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.security.AlgorithmParameters;
 import java.security.GeneralSecurityException;
@@ -386,7 +387,38 @@
     @Override
     protected final int engineUpdate(ByteBuffer input, ByteBuffer output)
             throws ShortBufferException {
-        return super.engineUpdate(input, output);
+        if (input == null) {
+            throw new NullPointerException("input == null");
+        }
+        if (output == null) {
+            throw new NullPointerException("output == null");
+        }
+
+        int inputSize = input.remaining();
+        byte[] outputArray;
+        if (input.hasArray()) {
+            outputArray =
+                    engineUpdate(
+                            input.array(), input.arrayOffset() + input.position(), inputSize);
+            input.position(input.position() + inputSize);
+        } else {
+            byte[] inputArray = new byte[inputSize];
+            input.get(inputArray);
+            outputArray = engineUpdate(inputArray, 0, inputSize);
+        }
+
+        int outputSize = (outputArray != null) ? outputArray.length : 0;
+        if (outputSize > 0) {
+            int outputBufferAvailable = output.remaining();
+            try {
+                output.put(outputArray);
+            } catch (BufferOverflowException e) {
+                throw new ShortBufferException(
+                        "Output buffer too small. Produced: " + outputSize + ", available: "
+                                + outputBufferAvailable);
+            }
+        }
+        return outputSize;
     }
 
     @Override
@@ -512,7 +544,38 @@
     @Override
     protected final int engineDoFinal(ByteBuffer input, ByteBuffer output)
             throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
-        return super.engineDoFinal(input, output);
+        if (input == null) {
+            throw new NullPointerException("input == null");
+        }
+        if (output == null) {
+            throw new NullPointerException("output == null");
+        }
+
+        int inputSize = input.remaining();
+        byte[] outputArray;
+        if (input.hasArray()) {
+            outputArray =
+                    engineDoFinal(
+                            input.array(), input.arrayOffset() + input.position(), inputSize);
+            input.position(input.position() + inputSize);
+        } else {
+            byte[] inputArray = new byte[inputSize];
+            input.get(inputArray);
+            outputArray = engineDoFinal(inputArray, 0, inputSize);
+        }
+
+        int outputSize = (outputArray != null) ? outputArray.length : 0;
+        if (outputSize > 0) {
+            int outputBufferAvailable = output.remaining();
+            try {
+                output.put(outputArray);
+            } catch (BufferOverflowException e) {
+                throw new ShortBufferException(
+                        "Output buffer too small. Produced: " + outputSize + ", available: "
+                                + outputBufferAvailable);
+            }
+        }
+        return outputSize;
     }
 
     @Override
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index cc68fb2..fc4916c 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -101,6 +101,7 @@
 
 ifeq (true, $(HWUI_NEW_OPS))
     hwui_src_files += \
+        BakedOpDispatcher.cpp \
         BakedOpRenderer.cpp \
         OpReorderer.cpp \
         RecordingCanvas.cpp
@@ -275,7 +276,9 @@
 LOCAL_MODULE_STEM_32 := hwuimicro
 LOCAL_MODULE_STEM_64 := hwuimicro64
 LOCAL_SHARED_LIBRARIES := $(hwui_shared_libraries)
-LOCAL_CFLAGS := $(hwui_cflags)
+LOCAL_CFLAGS := \
+        $(hwui_cflags) \
+        -DHWUI_NULL_GPU
 LOCAL_C_INCLUDES += bionic/benchmarks/
 
 LOCAL_WHOLE_STATIC_LIBRARIES := libhwui_static_null_gpu
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
new file mode 100644
index 0000000..b56b1e4
--- /dev/null
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2015 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 "BakedOpDispatcher.h"
+
+#include "BakedOpRenderer.h"
+#include "Caches.h"
+#include "Glop.h"
+#include "GlopBuilder.h"
+#include "renderstate/OffscreenBufferPool.h"
+#include "renderstate/RenderState.h"
+#include "utils/GLUtils.h"
+#include "VertexBuffer.h"
+
+#include <algorithm>
+#include <math.h>
+
+namespace android {
+namespace uirenderer {
+
+void BakedOpDispatcher::onRenderNodeOp(BakedOpRenderer&, const RenderNodeOp&, const BakedOpState&) {
+    LOG_ALWAYS_FATAL("unsupported operation");
+}
+
+void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer& renderer, const BeginLayerOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("unsupported operation");
+}
+
+void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer& renderer, const EndLayerOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("unsupported operation");
+}
+
+void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) {
+    renderer.caches().textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere?
+    Texture* texture = renderer.getTexture(op.bitmap);
+    if (!texture) return;
+    const AutoTexture autoCleanup(texture);
+
+    const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType)
+            ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshTexturedUnitQuad(texture->uvMapper)
+            .setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height))
+            .build();
+    renderer.renderGlop(state, glop);
+}
+
+void BakedOpDispatcher::onLinesOp(BakedOpRenderer& renderer, const LinesOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("todo");
+}
+
+void BakedOpDispatcher::onRectOp(BakedOpRenderer& renderer, const RectOp& op, const BakedOpState& state) {
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshUnitQuad()
+            .setFillPaint(*op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewMapUnitToRect(op.unmappedBounds)
+            .build();
+    renderer.renderGlop(state, glop);
+}
+
+namespace VertexBufferRenderFlags {
+    enum {
+        Offset = 0x1,
+        ShadowInterp = 0x2,
+    };
+}
+
+static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state,
+        const VertexBuffer& vertexBuffer, float translateX, float translateY,
+        SkPaint& paint, int vertexBufferRenderFlags) {
+    if (CC_LIKELY(vertexBuffer.getVertexCount())) {
+        bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp;
+        const int transformFlags = TransformFlags::OffsetByFudgeFactor;
+        Glop glop;
+        GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+                .setRoundRectClipState(state.roundRectClipState)
+                .setMeshVertexBuffer(vertexBuffer, shadowInterp)
+                .setFillPaint(paint, state.alpha)
+                .setTransform(state.computedState.transform, transformFlags)
+                .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
+                .build();
+        renderer.renderGlop(state, glop);
+    }
+}
+
+static void renderShadow(BakedOpRenderer& renderer, const BakedOpState& state, float casterAlpha,
+        const VertexBuffer* ambientShadowVertexBuffer, const VertexBuffer* spotShadowVertexBuffer) {
+    SkPaint paint;
+    paint.setAntiAlias(true); // want to use AlphaVertex
+
+    // The caller has made sure casterAlpha > 0.
+    uint8_t ambientShadowAlpha = renderer.getLightInfo().ambientShadowAlpha;
+    if (CC_UNLIKELY(Properties::overrideAmbientShadowStrength >= 0)) {
+        ambientShadowAlpha = Properties::overrideAmbientShadowStrength;
+    }
+    if (ambientShadowVertexBuffer && ambientShadowAlpha > 0) {
+        paint.setAlpha((uint8_t)(casterAlpha * ambientShadowAlpha));
+        renderVertexBuffer(renderer, state, *ambientShadowVertexBuffer, 0, 0,
+                paint, VertexBufferRenderFlags::ShadowInterp);
+    }
+
+    uint8_t spotShadowAlpha = renderer.getLightInfo().spotShadowAlpha;
+    if (CC_UNLIKELY(Properties::overrideSpotShadowStrength >= 0)) {
+        spotShadowAlpha = Properties::overrideSpotShadowStrength;
+    }
+    if (spotShadowVertexBuffer && spotShadowAlpha > 0) {
+        paint.setAlpha((uint8_t)(casterAlpha * spotShadowAlpha));
+        renderVertexBuffer(renderer, state, *spotShadowVertexBuffer, 0, 0,
+                paint, VertexBufferRenderFlags::ShadowInterp);
+    }
+}
+
+void BakedOpDispatcher::onShadowOp(BakedOpRenderer& renderer, const ShadowOp& op, const BakedOpState& state) {
+    TessellationCache::vertexBuffer_pair_t buffers;
+    renderer.caches().tessellationCache.getShadowBuffers(&state.computedState.transform,
+            op.localClipRect, op.casterAlpha >= 1.0f, op.casterPath,
+            &op.shadowMatrixXY, &op.shadowMatrixZ,
+            op.lightCenter, renderer.getLightInfo().lightRadius,
+            buffers);
+
+    renderShadow(renderer, state, op.casterAlpha, buffers.first, buffers.second);
+}
+
+void BakedOpDispatcher::onSimpleRectsOp(BakedOpRenderer& renderer, const SimpleRectsOp& op, const BakedOpState& state) {
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshIndexedQuads(&op.vertices[0], op.vertexCount / 4)
+            .setFillPaint(*op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewOffsetRect(0, 0, op.unmappedBounds)
+            .build();
+    renderer.renderGlop(state, glop);
+}
+
+static void renderTextShadow(BakedOpRenderer& renderer, FontRenderer& fontRenderer,
+        const TextOp& op, const BakedOpState& state) {
+    renderer.caches().textureState().activateTexture(0);
+
+    PaintUtils::TextShadow textShadow;
+    if (!PaintUtils::getTextShadow(op.paint, &textShadow)) {
+        LOG_ALWAYS_FATAL("failed to query shadow attributes");
+    }
+
+    renderer.caches().dropShadowCache.setFontRenderer(fontRenderer);
+    ShadowTexture* texture = renderer.caches().dropShadowCache.get(
+            op.paint, (const char*) op.glyphs,
+            op.glyphCount, textShadow.radius, op.positions);
+    // If the drop shadow exceeds the max texture size or couldn't be
+    // allocated, skip drawing
+    if (!texture) return;
+    const AutoTexture autoCleanup(texture);
+
+    const float sx = op.x - texture->left + textShadow.dx;
+    const float sy = op.y - texture->top + textShadow.dy;
+
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshTexturedUnitQuad(nullptr)
+            .setFillShadowTexturePaint(*texture, textShadow.color, *op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height))
+            .build();
+    renderer.renderGlop(state, glop);
+}
+
+void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state) {
+    FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer();
+
+    if (CC_UNLIKELY(PaintUtils::hasTextShadow(op.paint))) {
+        fontRenderer.setFont(op.paint, SkMatrix::I());
+        renderTextShadow(renderer, fontRenderer, op, state);
+    }
+
+    float x = op.x;
+    float y = op.y;
+    const Matrix4& transform = state.computedState.transform;
+    const bool pureTranslate = transform.isPureTranslate();
+    if (CC_LIKELY(pureTranslate)) {
+        x = floorf(x + transform.getTranslateX() + 0.5f);
+        y = floorf(y + transform.getTranslateY() + 0.5f);
+        fontRenderer.setFont(op.paint, SkMatrix::I());
+        fontRenderer.setTextureFiltering(false);
+    } else if (CC_UNLIKELY(transform.isPerspective())) {
+        fontRenderer.setFont(op.paint, SkMatrix::I());
+        fontRenderer.setTextureFiltering(true);
+    } else {
+        // We only pass a partial transform to the font renderer. That partial
+        // matrix defines how glyphs are rasterized. Typically we want glyphs
+        // to be rasterized at their final size on screen, which means the partial
+        // matrix needs to take the scale factor into account.
+        // When a partial matrix is used to transform glyphs during rasterization,
+        // the mesh is generated with the inverse transform (in the case of scale,
+        // the mesh is generated at 1.0 / scale for instance.) This allows us to
+        // apply the full transform matrix at draw time in the vertex shader.
+        // Applying the full matrix in the shader is the easiest way to handle
+        // rotation and perspective and allows us to always generated quads in the
+        // font renderer which greatly simplifies the code, clipping in particular.
+        float sx, sy;
+        transform.decomposeScale(sx, sy);
+        fontRenderer.setFont(op.paint, SkMatrix::MakeScale(
+                roundf(std::max(1.0f, sx)),
+                roundf(std::max(1.0f, sy))));
+        fontRenderer.setTextureFiltering(true);
+    }
+
+    // TODO: Implement better clipping for scaled/rotated text
+    const Rect* clip = !pureTranslate ? nullptr : &state.computedState.clipRect;
+    Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
+
+    int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha;
+    SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint);
+    TextDrawFunctor functor(&renderer, &state, x, y, pureTranslate, alpha, mode, op.paint);
+
+    bool hasActiveLayer = false; // TODO
+    fontRenderer.renderPosText(op.paint, clip, (const char*) op.glyphs, op.glyphCount, x, y,
+            op.positions, hasActiveLayer ? &layerBounds : nullptr, &functor, true); // TODO: merging
+}
+
+void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) {
+    OffscreenBuffer* buffer = *op.layerHandle;
+
+    // TODO: extend this to handle HW layers & paint properties which
+    // reside in node.properties().layerProperties()
+    float layerAlpha = op.alpha * state.alpha;
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshTexturedIndexedVbo(buffer->vbo, buffer->elementCount)
+            .setFillLayer(buffer->texture, op.colorFilter, layerAlpha, op.mode, Blend::ModeOrderSwap::NoSwap)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewOffsetRectSnap(op.unmappedBounds.left, op.unmappedBounds.top,
+                    Rect(op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight()))
+            .build();
+    renderer.renderGlop(state, glop);
+
+    if (op.destroy) {
+        renderer.renderState().layerPool().putOrDelete(buffer);
+    }
+}
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/BakedOpDispatcher.h b/libs/hwui/BakedOpDispatcher.h
new file mode 100644
index 0000000..caf14bf
--- /dev/null
+++ b/libs/hwui/BakedOpDispatcher.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2015 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 ANDROID_HWUI_BAKED_OP_DISPATCHER_H
+#define ANDROID_HWUI_BAKED_OP_DISPATCHER_H
+
+#include "BakedOpState.h"
+#include "RecordedOp.h"
+
+namespace android {
+namespace uirenderer {
+
+/**
+ * Provides all "onBitmapOp(...)" style static methods for every op type, which convert the
+ * RecordedOps and their state to Glops, and renders them with the provided BakedOpRenderer.
+ *
+ * This dispatcher is separate from the renderer so that the dispatcher / renderer interaction is
+ * minimal through public BakedOpRenderer APIs.
+ */
+class BakedOpDispatcher {
+public:
+    // Declares all "onBitmapOp(...)" style methods for every op type
+#define DISPATCH_METHOD(Type) \
+        static void on##Type(BakedOpRenderer& renderer, const Type& op, const BakedOpState& state);
+    MAP_OPS(DISPATCH_METHOD);
+};
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_BAKED_OP_DISPATCHER_H
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp
index d13d7ef..6cdc320 100644
--- a/libs/hwui/BakedOpRenderer.cpp
+++ b/libs/hwui/BakedOpRenderer.cpp
@@ -25,15 +25,10 @@
 #include "VertexBuffer.h"
 
 #include <algorithm>
-#include <math.h>
 
 namespace android {
 namespace uirenderer {
 
-////////////////////////////////////////////////////////////////////////////////
-// BakedOpRenderer
-////////////////////////////////////////////////////////////////////////////////
-
 OffscreenBuffer* BakedOpRenderer::startTemporaryLayer(uint32_t width, uint32_t height) {
     LOG_ALWAYS_FATAL_IF(mRenderTarget.offscreenBuffer, "already has layer...");
 
@@ -151,238 +146,5 @@
     if (!mRenderTarget.frameBufferId) mHasDrawn = true;
 }
 
-////////////////////////////////////////////////////////////////////////////////
-// static BakedOpDispatcher methods
-////////////////////////////////////////////////////////////////////////////////
-
-void BakedOpDispatcher::onRenderNodeOp(BakedOpRenderer&, const RenderNodeOp&, const BakedOpState&) {
-    LOG_ALWAYS_FATAL("unsupported operation");
-}
-
-void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer& renderer, const BeginLayerOp& op, const BakedOpState& state) {
-    LOG_ALWAYS_FATAL("unsupported operation");
-}
-
-void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer& renderer, const EndLayerOp& op, const BakedOpState& state) {
-    LOG_ALWAYS_FATAL("unsupported operation");
-}
-
-void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) {
-    renderer.caches().textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere?
-    Texture* texture = renderer.getTexture(op.bitmap);
-    if (!texture) return;
-    const AutoTexture autoCleanup(texture);
-
-    const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType)
-            ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
-    Glop glop;
-    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-            .setRoundRectClipState(state.roundRectClipState)
-            .setMeshTexturedUnitQuad(texture->uvMapper)
-            .setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
-            .setTransform(state.computedState.transform, TransformFlags::None)
-            .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height))
-            .build();
-    renderer.renderGlop(state, glop);
-}
-
-void BakedOpDispatcher::onLinesOp(BakedOpRenderer& renderer, const LinesOp& op, const BakedOpState& state) {
-    LOG_ALWAYS_FATAL("todo");
-}
-
-void BakedOpDispatcher::onRectOp(BakedOpRenderer& renderer, const RectOp& op, const BakedOpState& state) {
-    Glop glop;
-    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-            .setRoundRectClipState(state.roundRectClipState)
-            .setMeshUnitQuad()
-            .setFillPaint(*op.paint, state.alpha)
-            .setTransform(state.computedState.transform, TransformFlags::None)
-            .setModelViewMapUnitToRect(op.unmappedBounds)
-            .build();
-    renderer.renderGlop(state, glop);
-}
-
-namespace VertexBufferRenderFlags {
-    enum {
-        Offset = 0x1,
-        ShadowInterp = 0x2,
-    };
-}
-
-static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state,
-        const VertexBuffer& vertexBuffer, float translateX, float translateY,
-        SkPaint& paint, int vertexBufferRenderFlags) {
-    if (CC_LIKELY(vertexBuffer.getVertexCount())) {
-        bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp;
-        const int transformFlags = TransformFlags::OffsetByFudgeFactor;
-        Glop glop;
-        GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-                .setRoundRectClipState(state.roundRectClipState)
-                .setMeshVertexBuffer(vertexBuffer, shadowInterp)
-                .setFillPaint(paint, state.alpha)
-                .setTransform(state.computedState.transform, transformFlags)
-                .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
-                .build();
-        renderer.renderGlop(state, glop);
-    }
-}
-
-static void renderShadow(BakedOpRenderer& renderer, const BakedOpState& state, float casterAlpha,
-        const VertexBuffer* ambientShadowVertexBuffer, const VertexBuffer* spotShadowVertexBuffer) {
-    SkPaint paint;
-    paint.setAntiAlias(true); // want to use AlphaVertex
-
-    // The caller has made sure casterAlpha > 0.
-    uint8_t ambientShadowAlpha = renderer.getLightInfo().ambientShadowAlpha;
-    if (CC_UNLIKELY(Properties::overrideAmbientShadowStrength >= 0)) {
-        ambientShadowAlpha = Properties::overrideAmbientShadowStrength;
-    }
-    if (ambientShadowVertexBuffer && ambientShadowAlpha > 0) {
-        paint.setAlpha((uint8_t)(casterAlpha * ambientShadowAlpha));
-        renderVertexBuffer(renderer, state, *ambientShadowVertexBuffer, 0, 0,
-                paint, VertexBufferRenderFlags::ShadowInterp);
-    }
-
-    uint8_t spotShadowAlpha = renderer.getLightInfo().spotShadowAlpha;
-    if (CC_UNLIKELY(Properties::overrideSpotShadowStrength >= 0)) {
-        spotShadowAlpha = Properties::overrideSpotShadowStrength;
-    }
-    if (spotShadowVertexBuffer && spotShadowAlpha > 0) {
-        paint.setAlpha((uint8_t)(casterAlpha * spotShadowAlpha));
-        renderVertexBuffer(renderer, state, *spotShadowVertexBuffer, 0, 0,
-                paint, VertexBufferRenderFlags::ShadowInterp);
-    }
-}
-
-void BakedOpDispatcher::onShadowOp(BakedOpRenderer& renderer, const ShadowOp& op, const BakedOpState& state) {
-    TessellationCache::vertexBuffer_pair_t buffers;
-    renderer.caches().tessellationCache.getShadowBuffers(&state.computedState.transform,
-            op.localClipRect, op.casterAlpha >= 1.0f, op.casterPath,
-            &op.shadowMatrixXY, &op.shadowMatrixZ,
-            op.lightCenter, renderer.getLightInfo().lightRadius,
-            buffers);
-
-    renderShadow(renderer, state, op.casterAlpha, buffers.first, buffers.second);
-}
-
-void BakedOpDispatcher::onSimpleRectsOp(BakedOpRenderer& renderer, const SimpleRectsOp& op, const BakedOpState& state) {
-    Glop glop;
-    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-            .setRoundRectClipState(state.roundRectClipState)
-            .setMeshIndexedQuads(&op.vertices[0], op.vertexCount / 4)
-            .setFillPaint(*op.paint, state.alpha)
-            .setTransform(state.computedState.transform, TransformFlags::None)
-            .setModelViewOffsetRect(0, 0, op.unmappedBounds)
-            .build();
-    renderer.renderGlop(state, glop);
-}
-
-static void renderTextShadow(BakedOpRenderer& renderer, FontRenderer& fontRenderer,
-        const TextOp& op, const BakedOpState& state) {
-    renderer.caches().textureState().activateTexture(0);
-
-    PaintUtils::TextShadow textShadow;
-    if (!PaintUtils::getTextShadow(op.paint, &textShadow)) {
-        LOG_ALWAYS_FATAL("failed to query shadow attributes");
-    }
-
-    renderer.caches().dropShadowCache.setFontRenderer(fontRenderer);
-    ShadowTexture* texture = renderer.caches().dropShadowCache.get(
-            op.paint, (const char*) op.glyphs,
-            op.glyphCount, textShadow.radius, op.positions);
-    // If the drop shadow exceeds the max texture size or couldn't be
-    // allocated, skip drawing
-    if (!texture) return;
-    const AutoTexture autoCleanup(texture);
-
-    const float sx = op.x - texture->left + textShadow.dx;
-    const float sy = op.y - texture->top + textShadow.dy;
-
-    Glop glop;
-    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-            .setRoundRectClipState(state.roundRectClipState)
-            .setMeshTexturedUnitQuad(nullptr)
-            .setFillShadowTexturePaint(*texture, textShadow.color, *op.paint, state.alpha)
-            .setTransform(state.computedState.transform, TransformFlags::None)
-            .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height))
-            .build();
-    renderer.renderGlop(state, glop);
-}
-
-void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state) {
-    FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer();
-
-    if (CC_UNLIKELY(PaintUtils::hasTextShadow(op.paint))) {
-        fontRenderer.setFont(op.paint, SkMatrix::I());
-        renderTextShadow(renderer, fontRenderer, op, state);
-    }
-
-    float x = op.x;
-    float y = op.y;
-    const Matrix4& transform = state.computedState.transform;
-    const bool pureTranslate = transform.isPureTranslate();
-    if (CC_LIKELY(pureTranslate)) {
-        x = floorf(x + transform.getTranslateX() + 0.5f);
-        y = floorf(y + transform.getTranslateY() + 0.5f);
-        fontRenderer.setFont(op.paint, SkMatrix::I());
-        fontRenderer.setTextureFiltering(false);
-    } else if (CC_UNLIKELY(transform.isPerspective())) {
-        fontRenderer.setFont(op.paint, SkMatrix::I());
-        fontRenderer.setTextureFiltering(true);
-    } else {
-        // We only pass a partial transform to the font renderer. That partial
-        // matrix defines how glyphs are rasterized. Typically we want glyphs
-        // to be rasterized at their final size on screen, which means the partial
-        // matrix needs to take the scale factor into account.
-        // When a partial matrix is used to transform glyphs during rasterization,
-        // the mesh is generated with the inverse transform (in the case of scale,
-        // the mesh is generated at 1.0 / scale for instance.) This allows us to
-        // apply the full transform matrix at draw time in the vertex shader.
-        // Applying the full matrix in the shader is the easiest way to handle
-        // rotation and perspective and allows us to always generated quads in the
-        // font renderer which greatly simplifies the code, clipping in particular.
-        float sx, sy;
-        transform.decomposeScale(sx, sy);
-        fontRenderer.setFont(op.paint, SkMatrix::MakeScale(
-                roundf(std::max(1.0f, sx)),
-                roundf(std::max(1.0f, sy))));
-        fontRenderer.setTextureFiltering(true);
-    }
-
-    // TODO: Implement better clipping for scaled/rotated text
-    const Rect* clip = !pureTranslate ? nullptr : &state.computedState.clipRect;
-    Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
-
-    int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha;
-    SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint);
-    TextDrawFunctor functor(&renderer, &state, x, y, pureTranslate, alpha, mode, op.paint);
-
-    bool hasActiveLayer = false; // TODO
-    fontRenderer.renderPosText(op.paint, clip, (const char*) op.glyphs, op.glyphCount, x, y,
-            op.positions, hasActiveLayer ? &layerBounds : nullptr, &functor, true); // TODO: merging
-}
-
-void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) {
-    OffscreenBuffer* buffer = *op.layerHandle;
-
-    // TODO: extend this to handle HW layers & paint properties which
-    // reside in node.properties().layerProperties()
-    float layerAlpha = op.alpha * state.alpha;
-    Glop glop;
-    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-            .setRoundRectClipState(state.roundRectClipState)
-            .setMeshTexturedIndexedVbo(buffer->vbo, buffer->elementCount)
-            .setFillLayer(buffer->texture, op.colorFilter, layerAlpha, op.mode, Blend::ModeOrderSwap::NoSwap)
-            .setTransform(state.computedState.transform, TransformFlags::None)
-            .setModelViewOffsetRectSnap(op.unmappedBounds.left, op.unmappedBounds.top,
-                    Rect(op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight()))
-            .build();
-    renderer.renderGlop(state, glop);
-
-    if (op.destroy) {
-        renderer.renderState().layerPool().putOrDelete(buffer);
-    }
-}
-
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h
index 29f9a6f..62d1838 100644
--- a/libs/hwui/BakedOpRenderer.h
+++ b/libs/hwui/BakedOpRenderer.h
@@ -91,21 +91,6 @@
     const LightInfo mLightInfo;
 };
 
-/**
- * Provides all "onBitmapOp(...)" style static methods for every op type, which convert the
- * RecordedOps and their state to Glops, and renders them with the provided BakedOpRenderer.
- *
- * This dispatcher is separate from the renderer so that the dispatcher / renderer interaction is
- * minimal through public BakedOpRenderer APIs.
- */
-class BakedOpDispatcher {
-public:
-    // Declares all "onBitmapOp(...)" style methods for every op type
-#define DISPATCH_METHOD(Type) \
-        static void on##Type(BakedOpRenderer& renderer, const Type& op, const BakedOpState& state);
-    MAP_OPS(DISPATCH_METHOD);
-};
-
 }; // namespace uirenderer
 }; // namespace android
 
diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h
index 00c4e2d..60cc7ba 100644
--- a/libs/hwui/DisplayList.h
+++ b/libs/hwui/DisplayList.h
@@ -132,7 +132,7 @@
     DisplayList();
     ~DisplayList();
 
-    // index of DisplayListOp restore, after which projected descendents should be drawn
+    // index of DisplayListOp restore, after which projected descendants should be drawn
     int projectionReceiveIndex;
 
     const LsaVector<Chunk>& getChunks() const { return chunks; }
diff --git a/libs/hwui/DisplayListCanvas.h b/libs/hwui/DisplayListCanvas.h
index bf98f79..72fc100 100644
--- a/libs/hwui/DisplayListCanvas.h
+++ b/libs/hwui/DisplayListCanvas.h
@@ -55,6 +55,7 @@
 class DeferredLayerUpdater;
 class DisplayListOp;
 class DrawOp;
+class DrawRenderNodeOp;
 class RenderNode;
 class StateOp;
 
diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h
index 977b53c..bd11d0a 100644
--- a/libs/hwui/DisplayListOp.h
+++ b/libs/hwui/DisplayListOp.h
@@ -1386,19 +1386,19 @@
             : DrawBoundedOp(0, 0, renderNode->getWidth(), renderNode->getHeight(), nullptr)
             , renderNode(renderNode)
             , mRecordedWithPotentialStencilClip(!clipIsSimple || !transformFromParent.isSimple())
-            , mTransformFromParent(transformFromParent)
-            , mSkipInOrderDraw(false) {}
+            , localMatrix(transformFromParent)
+            , skipInOrderDraw(false) {}
 
     virtual void defer(DeferStateStruct& deferStruct, int saveCount, int level,
             bool useQuickReject) override {
-        if (renderNode->isRenderable() && !mSkipInOrderDraw) {
+        if (renderNode->isRenderable() && !skipInOrderDraw) {
             renderNode->defer(deferStruct, level + 1);
         }
     }
 
     virtual void replay(ReplayStateStruct& replayStruct, int saveCount, int level,
             bool useQuickReject) override {
-        if (renderNode->isRenderable() && !mSkipInOrderDraw) {
+        if (renderNode->isRenderable() && !skipInOrderDraw) {
             renderNode->replay(replayStruct, level + 1);
         }
     }
@@ -1439,7 +1439,7 @@
     /**
      * Records transform vs parent, used for computing total transform without rerunning DL contents
      */
-    const mat4 mTransformFromParent;
+    const mat4 localMatrix;
 
     /**
      * Holds the transformation between the projection surface ViewGroup and this RenderNode
@@ -1449,8 +1449,8 @@
      *
      * Note: doesn't include transformation within the RenderNode, or its properties.
      */
-    mat4 mTransformFromCompositingAncestor;
-    bool mSkipInOrderDraw;
+    mat4 transformFromCompositingAncestor;
+    bool skipInOrderDraw;
 };
 
 /**
diff --git a/libs/hwui/FontRenderer.cpp b/libs/hwui/FontRenderer.cpp
index 5f33cae..47654f4 100644
--- a/libs/hwui/FontRenderer.cpp
+++ b/libs/hwui/FontRenderer.cpp
@@ -29,8 +29,9 @@
 
 
 #if HWUI_NEW_OPS
-#include "BakedOpState.h"
+#include "BakedOpDispatcher.h"
 #include "BakedOpRenderer.h"
+#include "BakedOpState.h"
 #else
 #include "OpenGLRenderer.h"
 #endif
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
index 5e954ae..9cbd9c2d 100644
--- a/libs/hwui/OpReorderer.cpp
+++ b/libs/hwui/OpReorderer.cpp
@@ -330,6 +330,7 @@
     for (int i = layers.entries().size() - 1; i >= 0; i--) {
         RenderNode* layerNode = layers.entries()[i].renderNode;
         const Rect& layerDamage = layers.entries()[i].damage;
+        layerNode->computeOrdering();
 
         // map current light center into RenderNode's coordinate space
         Vector3 lightCenter = mCanvasState.currentSnapshot()->getRelativeLightCenter();
@@ -339,7 +340,7 @@
                 layerDamage, lightCenter, nullptr, layerNode);
 
         if (layerNode->getDisplayList()) {
-            deferDisplayList(*(layerNode->getDisplayList()));
+            deferNodeOps(*layerNode);
         }
         restoreForLayer();
     }
@@ -347,6 +348,7 @@
     // Defer Fbo0
     for (const sp<RenderNode>& node : nodes) {
         if (node->nothingToDraw()) continue;
+        node->computeOrdering();
 
         int count = mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag);
         deferNodePropsAndOps(*node);
@@ -354,20 +356,6 @@
     }
 }
 
-OpReorderer::OpReorderer(int viewportWidth, int viewportHeight, const DisplayList& displayList,
-        const Vector3& lightCenter)
-        : mCanvasState(*this) {
-    ATRACE_NAME("prepare drawing commands");
-    // Prepare to defer Fbo0
-    mLayerReorderers.emplace_back(viewportWidth, viewportHeight,
-            Rect(viewportWidth, viewportHeight));
-    mLayerStack.push_back(0);
-    mCanvasState.initializeSaveStack(viewportWidth, viewportHeight,
-            0, 0, viewportWidth, viewportHeight, lightCenter);
-
-    deferDisplayList(displayList);
-}
-
 void OpReorderer::onViewportInitialized() {}
 
 void OpReorderer::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {}
@@ -462,10 +450,10 @@
                     Matrix4::identity(),
                     saveLayerBounds,
                     &saveLayerPaint));
-            deferDisplayList(*(node.getDisplayList()));
+            deferNodeOps(node);
             onEndLayerOp(*new (mAllocator) EndLayerOp());
         } else {
-            deferDisplayList(*(node.getDisplayList()));
+            deferNodeOps(node);
         }
     }
 }
@@ -610,18 +598,53 @@
     }
 }
 
+void OpReorderer::deferProjectedChildren(const RenderNode& renderNode) {
+    const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath();
+    int count = mCanvasState.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+
+    // can't be null, since DL=null node rejection happens before deferNodePropsAndOps
+    const DisplayList& displayList = *(renderNode.getDisplayList());
+
+    const RecordedOp* op = (displayList.getOps()[displayList.projectionReceiveIndex]);
+    const RenderNodeOp* backgroundOp = static_cast<const RenderNodeOp*>(op);
+    const RenderProperties& backgroundProps = backgroundOp->renderNode->properties();
+
+    // Transform renderer to match background we're projecting onto
+    // (by offsetting canvas by translationX/Y of background rendernode, since only those are set)
+    mCanvasState.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY());
+
+    // If the projection receiver has an outline, we mask projected content to it
+    // (which we know, apriori, are all tessellated paths)
+    mCanvasState.setProjectionPathMask(mAllocator, projectionReceiverOutline);
+
+    // draw projected nodes
+    for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) {
+        RenderNodeOp* childOp = renderNode.mProjectedNodes[i];
+
+        int restoreTo = mCanvasState.save(SkCanvas::kMatrix_SaveFlag);
+        mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor);
+        deferRenderNodeOp(*childOp);
+        mCanvasState.restoreToCount(restoreTo);
+    }
+
+    mCanvasState.restoreToCount(count);
+}
+
 /**
  * Used to define a list of lambdas referencing private OpReorderer::onXXXXOp() methods.
  *
- * This allows opIds embedded in the RecordedOps to be used for dispatching to these lambdas. E.g. a
- * BitmapOp op then would be dispatched to OpReorderer::onBitmapOp(const BitmapOp&)
+ * This allows opIds embedded in the RecordedOps to be used for dispatching to these lambdas.
+ * E.g. a BitmapOp op then would be dispatched to OpReorderer::onBitmapOp(const BitmapOp&)
  */
 #define OP_RECEIVER(Type) \
         [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); },
-void OpReorderer::deferDisplayList(const DisplayList& displayList) {
+void OpReorderer::deferNodeOps(const RenderNode& renderNode) {
     static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = {
         MAP_OPS(OP_RECEIVER)
     };
+
+    // can't be null, since DL=null node rejection happens before deferNodePropsAndOps
+    const DisplayList& displayList = *(renderNode.getDisplayList());
     for (const DisplayList::Chunk& chunk : displayList.getChunks()) {
         FatVector<ZRenderNodeOpPair, 16> zTranslatedNodes;
         buildZSortedChildList(&zTranslatedNodes, displayList, chunk);
@@ -630,6 +653,12 @@
         for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) {
             const RecordedOp* op = displayList.getOps()[opIndex];
             receivers[op->opId](*this, *op);
+
+            if (CC_UNLIKELY(!renderNode.mProjectedNodes.empty()
+                    && displayList.projectionReceiveIndex >= 0
+                    && static_cast<int>(opIndex) == displayList.projectionReceiveIndex)) {
+                deferProjectedChildren(renderNode);
+            }
         }
         defer3dChildren(ChildrenSelectMode::Positive, zTranslatedNodes);
     }
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
index 976f413..00df8b0 100644
--- a/libs/hwui/OpReorderer.h
+++ b/libs/hwui/OpReorderer.h
@@ -124,9 +124,6 @@
             uint32_t viewportWidth, uint32_t viewportHeight,
             const std::vector< sp<RenderNode> >& nodes, const Vector3& lightCenter);
 
-    OpReorderer(int viewportWidth, int viewportHeight, const DisplayList& displayList,
-            const Vector3& lightCenter);
-
     virtual ~OpReorderer() {}
 
     /**
@@ -202,16 +199,18 @@
         return BakedOpState::tryConstruct(mAllocator, *mCanvasState.currentSnapshot(), recordedOp);
     }
 
-    // should always be surrounded by a save/restore pair
+    // should always be surrounded by a save/restore pair, and not called if DisplayList is null
     void deferNodePropsAndOps(RenderNode& node);
 
-    void deferShadow(const RenderNodeOp& casterOp);
-
-    void deferDisplayList(const DisplayList& displayList);
-
     template <typename V>
     void defer3dChildren(ChildrenSelectMode mode, const V& zTranslatedNodes);
 
+    void deferShadow(const RenderNodeOp& casterOp);
+
+    void deferProjectedChildren(const RenderNode& renderNode);
+
+    void deferNodeOps(const RenderNode& renderNode);
+
     void deferRenderNodeOp(const RenderNodeOp& op);
 
     void replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers);
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index 127dca5..b4a201e 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -97,7 +97,17 @@
     RenderNodeOp(BASE_PARAMS_PAINTLESS, RenderNode* renderNode)
             : SUPER_PAINTLESS(RenderNodeOp)
             , renderNode(renderNode) {}
-    RenderNode * renderNode; // not const, since drawing modifies it (somehow...)
+    RenderNode * renderNode; // not const, since drawing modifies it
+
+    /**
+     * Holds the transformation between the projection surface ViewGroup and this RenderNode
+     * 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 transformation within the RenderNode, or its properties.
+     */
+    Matrix4 transformFromCompositingAncestor;
     bool skipInOrderDraw = false;
 };
 
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 736cc9e..6d0e9e0 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -205,10 +205,6 @@
         return dstBuffer;
     }
 
-    inline char* refText(const char* text, size_t byteLength) {
-        return (char*) refBuffer<uint8_t>((uint8_t*)text, byteLength);
-    }
-
     inline const SkPath* refPath(const SkPath* path) {
         if (!path) return nullptr;
 
@@ -220,13 +216,8 @@
     }
 
     /**
-     * Returns a RenderThread-safe, const copy of the SkPaint parameter passed in (with deduping
-     * based on paint generation ID)
-     *
-     * Note that this forces Left_Align, since drawText glyph rendering expects left alignment,
-     * since alignment offsetting has been done at a higher level. This is done to essentially all
-     * copied paints, since the deduping can mean a paint is shared by drawText commands and other
-     * types (which wouldn't care about alignment).
+     * Returns a RenderThread-safe, const copy of the SkPaint parameter passed in
+     * (with deduping based on paint hash / equality check)
      */
     inline const SkPaint* refPaint(const SkPaint* paint) {
         if (!paint) return nullptr;
@@ -246,11 +237,8 @@
         // In the unlikely event that 2 unique paints have the same hash we do a
         // object equality check to ensure we don't erroneously dedup them.
         if (cachedPaint == nullptr || *cachedPaint != *paint) {
-            SkPaint* copy = new SkPaint(*paint);
-            copy->setTextAlign(SkPaint::kLeft_Align);
-
-            cachedPaint = copy;
-            mDisplayList->paints.emplace_back(copy);
+            cachedPaint = new SkPaint(*paint);
+            mDisplayList->paints.emplace_back(cachedPaint);
             // replaceValueFor() performs an add if the entry doesn't exist
             mPaintMap.replaceValueFor(key, cachedPaint);
             refBitmapsInShader(cachedPaint->getShader());
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 3f24f44..ae690fd 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -487,7 +487,7 @@
             info.damageAccumulator->pushTransform(&op->localMatrix);
             bool childFunctorsNeedLayer = functorsNeedLayer; // TODO! || op->mRecordedWithPotentialStencilClip;
 #else
-            info.damageAccumulator->pushTransform(&op->mTransformFromParent);
+            info.damageAccumulator->pushTransform(&op->localMatrix);
             bool childFunctorsNeedLayer = functorsNeedLayer
                     // Recorded with non-rect clip, or canvas-rotated by parent
                     || op->mRecordedWithPotentialStencilClip;
@@ -658,7 +658,6 @@
  * which are flagged to not draw in the standard draw loop.
  */
 void RenderNode::computeOrdering() {
-#if !HWUI_NEW_OPS
     ATRACE_CALL();
     mProjectedNodes.clear();
 
@@ -666,43 +665,41 @@
     // transform properties are applied correctly to top level children
     if (mDisplayList == nullptr) return;
     for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) {
-        DrawRenderNodeOp* childOp = mDisplayList->getChildren()[i];
+        renderNodeOp_t* childOp = mDisplayList->getChildren()[i];
         childOp->renderNode->computeOrderingImpl(childOp, &mProjectedNodes, &mat4::identity());
     }
-#endif
 }
 
 void RenderNode::computeOrderingImpl(
-        DrawRenderNodeOp* opState,
-        std::vector<DrawRenderNodeOp*>* compositedChildrenOfProjectionSurface,
+        renderNodeOp_t* opState,
+        std::vector<renderNodeOp_t*>* compositedChildrenOfProjectionSurface,
         const mat4* transformFromProjectionSurface) {
-#if !HWUI_NEW_OPS
     mProjectedNodes.clear();
     if (mDisplayList == nullptr || mDisplayList->isEmpty()) return;
 
     // TODO: should avoid this calculation in most cases
     // TODO: just calculate single matrix, down to all leaf composited elements
     Matrix4 localTransformFromProjectionSurface(*transformFromProjectionSurface);
-    localTransformFromProjectionSurface.multiply(opState->mTransformFromParent);
+    localTransformFromProjectionSurface.multiply(opState->localMatrix);
 
     if (properties().getProjectBackwards()) {
         // composited projectee, flag for out of order draw, save matrix, and store in proj surface
-        opState->mSkipInOrderDraw = true;
-        opState->mTransformFromCompositingAncestor = localTransformFromProjectionSurface;
+        opState->skipInOrderDraw = true;
+        opState->transformFromCompositingAncestor = localTransformFromProjectionSurface;
         compositedChildrenOfProjectionSurface->push_back(opState);
     } else {
         // standard in order draw
-        opState->mSkipInOrderDraw = false;
+        opState->skipInOrderDraw = false;
     }
 
     if (mDisplayList->getChildren().size() > 0) {
         const bool isProjectionReceiver = mDisplayList->projectionReceiveIndex >= 0;
         bool haveAppliedPropertiesToProjection = false;
         for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) {
-            DrawRenderNodeOp* childOp = mDisplayList->getChildren()[i];
+            renderNodeOp_t* childOp = mDisplayList->getChildren()[i];
             RenderNode* child = childOp->renderNode;
 
-            std::vector<DrawRenderNodeOp*>* projectionChildren = nullptr;
+            std::vector<renderNodeOp_t*>* projectionChildren = nullptr;
             const mat4* projectionTransform = nullptr;
             if (isProjectionReceiver && !child->properties().getProjectBackwards()) {
                 // if receiving projections, collect projecting descendant
@@ -723,7 +720,6 @@
             child->computeOrderingImpl(childOp, projectionChildren, projectionTransform);
         }
     }
-#endif
 }
 
 class DeferOperationHandler {
@@ -793,10 +789,10 @@
 
         if (!MathUtils::isZero(childZ) && chunk.reorderChildren) {
             zTranslatedNodes.push_back(ZDrawRenderNodeOpPair(childZ, childOp));
-            childOp->mSkipInOrderDraw = true;
+            childOp->skipInOrderDraw = true;
         } else if (!child->properties().getProjectBackwards()) {
             // regular, in order drawing DisplayList
-            childOp->mSkipInOrderDraw = false;
+            childOp->skipInOrderDraw = false;
         }
     }
 
@@ -913,7 +909,7 @@
             // 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) {
-                caster->issueDrawShadowOperation(casterOp->mTransformFromParent, handler);
+                caster->issueDrawShadowOperation(casterOp->localMatrix, handler);
 
                 lastCasterZ = casterZ; // must do this even if current caster not casting a shadow
                 shadowIndex++;
@@ -927,10 +923,10 @@
 
         DrawRenderNodeOp* childOp = zTranslatedNodes[drawIndex].value;
 
-        renderer.concatMatrix(childOp->mTransformFromParent);
-        childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone
+        renderer.concatMatrix(childOp->localMatrix);
+        childOp->skipInOrderDraw = false; // this is horrible, I'm so sorry everyone
         handler(childOp, renderer.getSaveCount() - 1, properties().getClipToBounds());
-        childOp->mSkipInOrderDraw = true;
+        childOp->skipInOrderDraw = true;
 
         renderer.restoreToCount(restoreTo);
         drawIndex++;
@@ -967,14 +963,14 @@
 
     // draw projected nodes
     for (size_t i = 0; i < mProjectedNodes.size(); i++) {
-        DrawRenderNodeOp* childOp = mProjectedNodes[i];
+        renderNodeOp_t* childOp = mProjectedNodes[i];
 
         // matrix save, concat, and restore can be done safely without allocating operations
         int restoreTo = renderer.save(SkCanvas::kMatrix_SaveFlag);
-        renderer.concatMatrix(childOp->mTransformFromCompositingAncestor);
-        childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone
+        renderer.concatMatrix(childOp->transformFromCompositingAncestor);
+        childOp->skipInOrderDraw = false; // this is horrible, I'm so sorry everyone
         handler(childOp, renderer.getSaveCount() - 1, properties().getClipToBounds());
-        childOp->mSkipInOrderDraw = true;
+        childOp->skipInOrderDraw = true;
         renderer.restoreToCount(restoreTo);
     }
 
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 83d1b58..b6f50b1 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -51,20 +51,22 @@
 class Rect;
 class SkiaShader;
 
-
 #if HWUI_NEW_OPS
 class OffscreenBuffer;
+struct RenderNodeOp;
 typedef OffscreenBuffer layer_t;
+typedef RenderNodeOp renderNodeOp_t;
 #else
 class Layer;
 typedef Layer layer_t;
+typedef DrawRenderNodeOp renderNodeOp_t;
 #endif
 
 class ClipRectOp;
+class DrawRenderNodeOp;
 class SaveLayerOp;
 class SaveOp;
 class RestoreToCountOp;
-class DrawRenderNodeOp;
 class TreeInfo;
 
 namespace proto {
@@ -85,6 +87,7 @@
  */
 class RenderNode : public VirtualLightRefBase {
 friend class TestUtils; // allow TestUtils to access syncDisplayList / syncProperties
+friend class OpReorderer;
 public:
     enum DirtyPropertyMask {
         GENERIC         = 1 << 1,
@@ -221,8 +224,8 @@
         PositiveZChildren
     };
 
-    void computeOrderingImpl(DrawRenderNodeOp* opState,
-            std::vector<DrawRenderNodeOp*>* compositedChildrenOfProjectionSurface,
+    void computeOrderingImpl(renderNodeOp_t* opState,
+            std::vector<renderNodeOp_t*>* compositedChildrenOfProjectionSurface,
             const mat4* transformFromProjectionSurface);
 
     template <class T>
@@ -305,7 +308,7 @@
      */
 
     // for projection surfaces, contains a list of all children items
-    std::vector<DrawRenderNodeOp*> mProjectedNodes;
+    std::vector<renderNodeOp_t*> mProjectedNodes;
 
     // How many references our parent(s) have to us. Typically this should alternate
     // between 2 and 1 (when a staging push happens we inc first then dec)
diff --git a/libs/hwui/microbench/OpReordererBench.cpp b/libs/hwui/microbench/OpReordererBench.cpp
index eea0c7f..53b64c3 100644
--- a/libs/hwui/microbench/OpReordererBench.cpp
+++ b/libs/hwui/microbench/OpReordererBench.cpp
@@ -17,11 +17,14 @@
 #include <benchmark/Benchmark.h>
 
 #include "BakedOpState.h"
+#include "BakedOpDispatcher.h"
 #include "BakedOpRenderer.h"
+#include "LayerUpdateQueue.h"
 #include "OpReorderer.h"
 #include "RecordedOp.h"
 #include "RecordingCanvas.h"
 #include "utils/TestUtils.h"
+#include "Vector.h"
 #include "microbench/MicroBench.h"
 
 #include <vector>
@@ -29,26 +32,38 @@
 using namespace android;
 using namespace android::uirenderer;
 
-auto sReorderingDisplayList = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
-    SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
-    SkPaint paint;
+const LayerUpdateQueue sEmptyLayerUpdateQueue;
+const Vector3 sLightCenter = {100, 100, 100};
 
-    // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects.
-    // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group.
-    canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
-    for (int i = 0; i < 30; i++) {
-        canvas.translate(0, 10);
-        canvas.drawRect(0, 0, 10, 10, paint);
-        canvas.drawBitmap(bitmap, 5, 0, nullptr);
-    }
-    canvas.restore();
-});
+static std::vector<sp<RenderNode>> createTestNodeList() {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+        SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
+        SkPaint paint;
+
+        // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects.
+        // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group.
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        for (int i = 0; i < 30; i++) {
+            canvas.translate(0, 10);
+            canvas.drawRect(0, 0, 10, 10, paint);
+            canvas.drawBitmap(bitmap, 5, 0, nullptr);
+        }
+        canvas.restore();
+    });
+    TestUtils::syncHierarchyPropertiesAndDisplayList(node);
+    std::vector<sp<RenderNode>> vec;
+    vec.emplace_back(node);
+    return vec;
+}
 
 BENCHMARK_NO_ARG(BM_OpReorderer_defer);
 void BM_OpReorderer_defer::Run(int iters) {
+    auto nodes = createTestNodeList();
     StartBenchmarkTiming();
     for (int i = 0; i < iters; i++) {
-        OpReorderer reorderer(200, 200, *sReorderingDisplayList, (Vector3) { 100, 100, 100 });
+        OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
+                nodes, sLightCenter);
         MicroBench::DoNotOptimize(&reorderer);
     }
     StopBenchmarkTiming();
@@ -57,13 +72,16 @@
 BENCHMARK_NO_ARG(BM_OpReorderer_deferAndRender);
 void BM_OpReorderer_deferAndRender::Run(int iters) {
     TestUtils::runOnRenderThread([this, iters](renderthread::RenderThread& thread) {
+        auto nodes = createTestNodeList();
+        BakedOpRenderer::LightInfo lightInfo = {50.0f, 128, 128 };
+
         RenderState& renderState = thread.renderState();
         Caches& caches = Caches::getInstance();
-        BakedOpRenderer::LightInfo lightInfo = { 50.0f, 128, 128 };
 
         StartBenchmarkTiming();
         for (int i = 0; i < iters; i++) {
-            OpReorderer reorderer(200, 200, *sReorderingDisplayList, (Vector3) { 100, 100, 100 });
+            OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
+                    nodes, sLightCenter);
 
             BakedOpRenderer renderer(caches, renderState, true, lightInfo);
             reorderer.replayBakedOps<BakedOpDispatcher>(renderer);
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index c3cfc94..d36ce99 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -28,6 +28,7 @@
 #include "renderthread/RenderThread.h"
 
 #if HWUI_NEW_OPS
+#include "BakedOpDispatcher.h"
 #include "BakedOpRenderer.h"
 #endif
 
diff --git a/libs/hwui/tests/scenes/OvalAnimation.cpp b/libs/hwui/tests/scenes/OvalAnimation.cpp
index 919a53d..936aba1 100644
--- a/libs/hwui/tests/scenes/OvalAnimation.cpp
+++ b/libs/hwui/tests/scenes/OvalAnimation.cpp
@@ -29,17 +29,14 @@
     sp<RenderNode> card;
     void createContent(int width, int height, TestCanvas& canvas) override {
         canvas.drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode);
-        canvas.insertReorderBarrier(true);
-
-        card = TestUtils::createNode(0, 0, 200, 200, [](TestCanvas& canvas) {
+        card = TestUtils::createNode(0, 0, 200, 200,
+                [](RenderProperties& props, TestCanvas& canvas) {
             SkPaint paint;
             paint.setAntiAlias(true);
             paint.setColor(0xFF000000);
             canvas.drawOval(0, 0, 200, 200, paint);
         });
-
         canvas.drawRenderNode(card.get());
-        canvas.insertReorderBarrier(false);
     }
 
     void doFrame(int frameNr) override {
diff --git a/libs/hwui/tests/scenes/PartialDamageAnimation.cpp b/libs/hwui/tests/scenes/PartialDamageAnimation.cpp
index 0fba4eb..c31ddd1 100644
--- a/libs/hwui/tests/scenes/PartialDamageAnimation.cpp
+++ b/libs/hwui/tests/scenes/PartialDamageAnimation.cpp
@@ -44,7 +44,7 @@
                 SkColor color = COLORS[static_cast<int>((y / dp(116))) % 4];
                 sp<RenderNode> card = TestUtils::createNode(x, y,
                         x + dp(100), y + dp(100),
-                        [color](TestCanvas& canvas) {
+                        [color](RenderProperties& props, TestCanvas& canvas) {
                     canvas.drawColor(color, SkXfermode::kSrcOver_Mode);
                 });
                 canvas.drawRenderNode(card.get());
diff --git a/libs/hwui/tests/scenes/RectGridAnimation.cpp b/libs/hwui/tests/scenes/RectGridAnimation.cpp
index 254f828..a1f04d6 100644
--- a/libs/hwui/tests/scenes/RectGridAnimation.cpp
+++ b/libs/hwui/tests/scenes/RectGridAnimation.cpp
@@ -34,7 +34,7 @@
         canvas.insertReorderBarrier(true);
 
         card = TestUtils::createNode(50, 50, 250, 250,
-                [](TestCanvas& canvas) {
+                [](RenderProperties& props, TestCanvas& canvas) {
             canvas.drawColor(0xFFFF00FF, SkXfermode::kSrcOver_Mode);
 
             SkRegion region;
diff --git a/libs/hwui/tests/scenes/SaveLayerAnimation.cpp b/libs/hwui/tests/scenes/SaveLayerAnimation.cpp
index c62dd19..c73e97b 100644
--- a/libs/hwui/tests/scenes/SaveLayerAnimation.cpp
+++ b/libs/hwui/tests/scenes/SaveLayerAnimation.cpp
@@ -32,7 +32,7 @@
         canvas.drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode); // background
 
         card = TestUtils::createNode(0, 0, 200, 200,
-                [](TestCanvas& canvas) {
+                [](RenderProperties& props, TestCanvas& canvas) {
             canvas.saveLayerAlpha(0, 0, 200, 200, 128, SkCanvas::kClipToLayer_SaveFlag);
             canvas.drawColor(0xFF00FF00, SkXfermode::kSrcOver_Mode); // outer, unclipped
             canvas.saveLayerAlpha(50, 50, 150, 150, 128, SkCanvas::kClipToLayer_SaveFlag);
diff --git a/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp b/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp
index 05fd08a..cc15cc6 100644
--- a/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp
+++ b/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp
@@ -31,7 +31,7 @@
 
 // sync node properties, so properties() reflects correct width and height
 static sp<RenderNode> createSyncedNode(uint32_t width, uint32_t height) {
-    sp<RenderNode> node = TestUtils::createNode(0, 0, width, height);
+    sp<RenderNode> node = TestUtils::createNode(0, 0, width, height, nullptr);
     TestUtils::syncHierarchyPropertiesAndDisplayList(node);
     return node;
 }
diff --git a/libs/hwui/unit_tests/OpReordererTests.cpp b/libs/hwui/unit_tests/OpReordererTests.cpp
index d76086c..2ce1d0a 100644
--- a/libs/hwui/unit_tests/OpReordererTests.cpp
+++ b/libs/hwui/unit_tests/OpReordererTests.cpp
@@ -111,25 +111,29 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 100, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkBitmap bitmap = TestUtils::createSkBitmap(25, 25);
         canvas.drawRect(0, 0, 100, 200, SkPaint());
         canvas.drawBitmap(bitmap, 10, 10, nullptr);
     });
-    OpReorderer reorderer(100, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
+            createSyncedNodeList(node), sLightCenter);
     SimpleTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(4, renderer.getIndex()); // 2 ops + start + end
 }
 
 TEST(OpReorderer, simpleRejection) {
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
         canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); // intersection should be empty
         canvas.drawRect(0, 0, 400, 400, SkPaint());
         canvas.restore();
     });
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
 
     FailRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
@@ -147,7 +151,8 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
 
         // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects.
@@ -161,7 +166,8 @@
         canvas.restore();
     });
 
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
     SimpleBatchingTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(2 * LOOPS, renderer.getIndex())
@@ -179,16 +185,19 @@
             EXPECT_TRUE(mIndex++ < LOOPS) << "Text should be beneath all strikethrough rects";
         }
     };
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 2000, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 2000,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkPaint textPaint;
         textPaint.setAntiAlias(true);
         textPaint.setTextSize(20);
         textPaint.setStrikeThruText(true);
+        textPaint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
         for (int i = 0; i < LOOPS; i++) {
             TestUtils::drawTextToCanvas(&canvas, "test text", textPaint, 10, 100 * (i + 1));
         }
     });
-    OpReorderer reorderer(200, 2000, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 2000), 200, 2000,
+            createSyncedNodeList(node), sLightCenter);
     TextStrikethroughTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(2 * LOOPS, renderer.getIndex())
@@ -214,14 +223,16 @@
         }
     };
 
-    sp<RenderNode> child = TestUtils::createNode(10, 10, 110, 110, [](RecordingCanvas& canvas) {
+    auto child = TestUtils::createNode(10, 10, 110, 110,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkPaint paint;
         paint.setColor(SK_ColorWHITE);
         canvas.drawRect(0, 0, 100, 100, paint);
     });
 
     RenderNode* childPtr = child.get();
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200, [childPtr](RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [childPtr](RenderProperties& props, RecordingCanvas& canvas) {
         SkPaint paint;
         paint.setColor(SK_ColorDKGRAY);
         canvas.drawRect(0, 0, 200, 200, paint);
@@ -249,7 +260,8 @@
         }
     };
 
-    sp<RenderNode> node = TestUtils::createNode(0, 0, 200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkBitmap bitmap = TestUtils::createSkBitmap(200, 200);
         canvas.drawBitmap(bitmap, 0, 0, nullptr);
     });
@@ -291,13 +303,14 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.saveLayerAlpha(10, 10, 190, 190, 128, SkCanvas::kClipToLayer_SaveFlag);
         canvas.drawRect(10, 10, 190, 190, SkPaint());
         canvas.restore();
     });
-
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
     SaveLayerSimpleTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(4, renderer.getIndex());
@@ -354,7 +367,8 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(800, 800, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 800, 800,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.saveLayerAlpha(0, 0, 800, 800, 128, SkCanvas::kClipToLayer_SaveFlag);
         {
             canvas.drawRect(0, 0, 800, 800, SkPaint());
@@ -367,14 +381,16 @@
         canvas.restore();
     });
 
-    OpReorderer reorderer(800, 800, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(800, 800), 800, 800,
+            createSyncedNodeList(node), sLightCenter);
     SaveLayerNestedTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(10, renderer.getIndex());
 }
 
 TEST(OpReorderer, saveLayerContentRejection) {
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        auto node = TestUtils::createNode(0, 0, 200, 200,
+                [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
         canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op);
         canvas.saveLayerAlpha(200, 200, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag);
@@ -385,7 +401,8 @@
         canvas.restore();
         canvas.restore();
     });
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
 
     FailRenderer renderer;
     // should see no ops, even within the layer, since the layer should be rejected
@@ -424,7 +441,7 @@
         }
     };
 
-    sp<RenderNode> node = TestUtils::createNode(10, 10, 110, 110,
+    auto node = TestUtils::createNode(10, 10, 110, 110,
             [](RenderProperties& props, RecordingCanvas& canvas) {
         props.mutateLayerProperties().setType(LayerType::RenderLayer);
         SkPaint paint;
@@ -562,7 +579,7 @@
 }
 static void drawOrderedNode(RecordingCanvas* canvas, uint8_t expectedDrawOrder, float z) {
     auto node = TestUtils::createNode(0, 0, 100, 100,
-            [expectedDrawOrder](RecordingCanvas& canvas) {
+            [expectedDrawOrder](RenderProperties& props, RecordingCanvas& canvas) {
         drawOrderedRect(&canvas, expectedDrawOrder);
     });
     node->mutateStagingProperties().setTranslationZ(z);
@@ -579,7 +596,7 @@
     };
 
     auto parent = TestUtils::createNode(0, 0, 100, 100,
-            [](RecordingCanvas& canvas) {
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder
         drawOrderedRect(&canvas, 1);
         canvas.insertReorderBarrier(true);
@@ -600,10 +617,93 @@
     EXPECT_EQ(10, renderer.getIndex());
 };
 
+TEST(OpReorderer, projectionReorder) {
+    static const int scrollX = 5;
+    static const int scrollY = 10;
+    class ProjectionReorderTestRenderer : public TestRendererBase {
+    public:
+        void onRectOp(const RectOp& op, const BakedOpState& state) override {
+            const int index = mIndex++;
+
+            Matrix4 expectedMatrix;
+            switch (index) {
+            case 0:
+                EXPECT_EQ(Rect(100, 100), op.unmappedBounds);
+                EXPECT_EQ(SK_ColorWHITE, op.paint->getColor());
+                expectedMatrix.loadIdentity();
+                break;
+            case 1:
+                EXPECT_EQ(Rect(-10, -10, 60, 60), op.unmappedBounds);
+                EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor());
+                expectedMatrix.loadTranslate(50, 50, 0); // TODO: should scroll be respected here?
+                break;
+            case 2:
+                EXPECT_EQ(Rect(100, 50), op.unmappedBounds);
+                EXPECT_EQ(SK_ColorBLUE, op.paint->getColor());
+                expectedMatrix.loadTranslate(-scrollX, 50 - scrollY, 0);
+                break;
+            default:
+                ADD_FAILURE();
+            }
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, state.computedState.transform);
+        }
+    };
+
+    /**
+     * Construct a tree of nodes, where the root (A) has a receiver background (B), and a child (C)
+     * with a projecting child (P) of its own. P would normally draw between B and C's "background"
+     * draw, but because it is projected backwards, it's drawn in between B and C.
+     *
+     * The parent is scrolled by scrollX/scrollY, but this does not affect the background
+     * (which isn't affected by scroll).
+     */
+    auto receiverBackground = TestUtils::createNode(0, 0, 100, 100,
+            [](RenderProperties& properties, RecordingCanvas& canvas) {
+        properties.setProjectionReceiver(true);
+        // scroll doesn't apply to background, so undone via translationX/Y
+        // NOTE: translationX/Y only! no other transform properties may be set for a proj receiver!
+        properties.setTranslationX(scrollX);
+        properties.setTranslationY(scrollY);
+
+        SkPaint paint;
+        paint.setColor(SK_ColorWHITE);
+        canvas.drawRect(0, 0, 100, 100, paint);
+    });
+    auto projectingRipple = TestUtils::createNode(50, 0, 100, 50,
+            [](RenderProperties& properties, RecordingCanvas& canvas) {
+        properties.setProjectBackwards(true);
+        properties.setClipToBounds(false);
+        SkPaint paint;
+        paint.setColor(SK_ColorDKGRAY);
+        canvas.drawRect(-10, -10, 60, 60, paint);
+    });
+    auto child = TestUtils::createNode(0, 50, 100, 100,
+            [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) {
+        SkPaint paint;
+        paint.setColor(SK_ColorBLUE);
+        canvas.drawRect(0, 0, 100, 50, paint);
+        canvas.drawRenderNode(projectingRipple.get());
+    });
+    auto parent = TestUtils::createNode(0, 0, 100, 100,
+            [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) {
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally)
+        canvas.drawRenderNode(receiverBackground.get());
+        canvas.drawRenderNode(child.get());
+        canvas.restore();
+    });
+
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 100), 100, 100,
+            createSyncedNodeList(parent), sLightCenter);
+    ProjectionReorderTestRenderer renderer;
+    reorderer.replayBakedOps<TestDispatcher>(renderer);
+    EXPECT_EQ(3, renderer.getIndex());
+}
+
 // creates a 100x100 shadow casting node with provided translationZ
 static sp<RenderNode> createWhiteRectShadowCaster(float translationZ) {
     return TestUtils::createNode(0, 0, 100, 100,
-            [translationZ] (RenderProperties& properties, RecordingCanvas& canvas) {
+            [translationZ](RenderProperties& properties, RecordingCanvas& canvas) {
         properties.setTranslationZ(translationZ);
         properties.mutableOutline().setRoundRect(0, 0, 100, 100, 0.0f, 1.0f);
         SkPaint paint;
@@ -630,8 +730,8 @@
         }
     };
 
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200,
-            [] (RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.insertReorderBarrier(true);
         canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get());
     });
@@ -666,8 +766,8 @@
         }
     };
 
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200,
-            [] (RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         // save/restore outside of reorderBarrier, so they don't get moved out of place
         canvas.translate(20, 10);
         int count = canvas.saveLayerAlpha(30, 50, 130, 150, 128, SkCanvas::kClipToLayer_SaveFlag);
@@ -706,7 +806,7 @@
         }
     };
 
-    sp<RenderNode> parent = TestUtils::createNode(50, 60, 150, 160,
+    auto parent = TestUtils::createNode(50, 60, 150, 160,
             [](RenderProperties& props, RecordingCanvas& canvas) {
         props.mutateLayerProperties().setType(LayerType::RenderLayer);
         canvas.insertReorderBarrier(true);
@@ -749,8 +849,8 @@
             EXPECT_TRUE(index == 2 || index == 3);
         }
     };
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200,
-            [] (RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.insertReorderBarrier(true);
         canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get());
         canvas.drawRenderNode(createWhiteRectShadowCaster(5.0001f).get());
@@ -954,7 +1054,7 @@
     SaveLayerAlphaData observedData;
     testSaveLayerAlphaClip(&observedData, [](RenderProperties& properties) {
         // Translate and rotate the view so that the only visible part is the top left corner of
-        // the view. It will form an isoceles right triangle with a long side length of 200 at the
+        // the view. It will form an isosceles right triangle with a long side length of 200 at the
         // bottom of the viewport.
         properties.setTranslationX(100);
         properties.setTranslationY(100);
diff --git a/libs/hwui/unit_tests/RecordingCanvasTests.cpp b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
index c23d47e..81f0851 100644
--- a/libs/hwui/unit_tests/RecordingCanvasTests.cpp
+++ b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
@@ -75,6 +75,7 @@
         SkPaint paint;
         paint.setAntiAlias(true);
         paint.setTextSize(20);
+        paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
         TestUtils::drawTextToCanvas(&canvas, "test text", paint, 25, 25);
     });
 
@@ -95,6 +96,7 @@
         SkPaint paint;
         paint.setAntiAlias(true);
         paint.setTextSize(20);
+        paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
         for (int i = 0; i < 2; i++) {
             for (int j = 0; j < 2; j++) {
                 paint.setUnderlineText(i != 0);
@@ -126,6 +128,7 @@
         SkPaint paint;
         paint.setAntiAlias(true);
         paint.setTextSize(20);
+        paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
         paint.setTextAlign(SkPaint::kLeft_Align);
         TestUtils::drawTextToCanvas(&canvas, "test text", paint, 25, 25);
         paint.setTextAlign(SkPaint::kCenter_Align);
@@ -135,12 +138,17 @@
     });
 
     int count = 0;
-    playbackOps(*dl, [&count](const RecordedOp& op) {
+    float lastX = FLT_MAX;
+    playbackOps(*dl, [&count, &lastX](const RecordedOp& op) {
         count++;
         ASSERT_EQ(RecordedOpId::TextOp, op.opId);
         EXPECT_EQ(SkPaint::kLeft_Align, op.paint->getTextAlign())
                 << "recorded drawText commands must force kLeft_Align on their paint";
-        EXPECT_EQ(SkPaint::kGlyphID_TextEncoding, op.paint->getTextEncoding()); // verify TestUtils
+
+        // verify TestUtils alignment offsetting (TODO: move asserts to Canvas base class)
+        EXPECT_GT(lastX, ((const TextOp&)op).x)
+                << "x coordinate should reduce across each of the draw commands, from alignment";
+        lastX = ((const TextOp&)op).x;
     });
     ASSERT_EQ(3, count);
 }
@@ -313,6 +321,49 @@
     EXPECT_EQ(3, count);
 }
 
+TEST(RecordingCanvas, drawRenderNode_projection) {
+    sp<RenderNode> background = TestUtils::createNode(50, 50, 150, 150,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+        SkPaint paint;
+        paint.setColor(SK_ColorWHITE);
+        canvas.drawRect(0, 0, 100, 100, paint);
+    });
+    {
+        background->mutateStagingProperties().setProjectionReceiver(false);
+
+        // NO RECEIVER PRESENT
+        auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200,
+                    [&background](RecordingCanvas& canvas) {
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+            canvas.drawRenderNode(background.get());
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+        });
+        EXPECT_EQ(-1, dl->projectionReceiveIndex)
+                << "no projection receiver should have been observed";
+    }
+    {
+        background->mutateStagingProperties().setProjectionReceiver(true);
+
+        // RECEIVER PRESENT
+        auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200,
+                    [&background](RecordingCanvas& canvas) {
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+            canvas.drawRenderNode(background.get());
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+        });
+
+        ASSERT_EQ(3u, dl->getOps().size()) << "Must be three ops";
+        auto op = dl->getOps()[1];
+        EXPECT_EQ(RecordedOpId::RenderNodeOp, op->opId);
+        EXPECT_EQ(1, dl->projectionReceiveIndex)
+                << "correct projection receiver not identified";
+
+        // verify the behavior works even though projection receiver hasn't been sync'd yet
+        EXPECT_TRUE(background->stagingProperties().isProjectionReceiver());
+        EXPECT_FALSE(background->properties().isProjectionReceiver());
+    }
+}
+
 TEST(RecordingCanvas, insertReorderBarrier) {
     auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
         canvas.drawRect(0, 0, 400, 400, SkPaint());
@@ -334,5 +385,39 @@
     EXPECT_TRUE(chunks[1].reorderChildren);
 }
 
+TEST(RecordingCanvas, refPaint) {
+    SkPaint paint;
+    paint.setAntiAlias(true);
+    paint.setTextSize(20);
+    paint.setTextAlign(SkPaint::kLeft_Align);
+    paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
+
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [&paint](RecordingCanvas& canvas) {
+        paint.setColor(SK_ColorBLUE);
+        // first three should use same paint
+        canvas.drawRect(0, 0, 200, 10, paint);
+        SkPaint paintCopy(paint);
+        canvas.drawRect(0, 10, 200, 20, paintCopy);
+        TestUtils::drawTextToCanvas(&canvas, "helloworld", paint, 50, 25);
+
+        // only here do we use different paint ptr
+        paint.setColor(SK_ColorRED);
+        canvas.drawRect(0, 20, 200, 30, paint);
+    });
+    auto ops = dl->getOps();
+    ASSERT_EQ(4u, ops.size());
+
+    // first three are the same
+    EXPECT_NE(nullptr, ops[0]->paint);
+    EXPECT_NE(&paint, ops[0]->paint);
+    EXPECT_EQ(ops[0]->paint, ops[1]->paint);
+    EXPECT_EQ(ops[0]->paint, ops[2]->paint);
+
+    // last is different, but still copied / non-null
+    EXPECT_NE(nullptr, ops[3]->paint);
+    EXPECT_NE(ops[0]->paint, ops[3]->paint);
+    EXPECT_NE(&paint, ops[3]->paint);
+}
+
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/utils/TestUtils.cpp b/libs/hwui/utils/TestUtils.cpp
index dd6fc36..6cef852 100644
--- a/libs/hwui/utils/TestUtils.cpp
+++ b/libs/hwui/utils/TestUtils.cpp
@@ -37,44 +37,56 @@
 }
 
 void TestUtils::drawTextToCanvas(TestCanvas* canvas, const char* text,
-        const SkPaint& inPaint, float x, float y) {
-   // copy to force TextEncoding (which JNI layer would have done)
-   SkPaint paint(inPaint);
-   paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
+        const SkPaint& paint, float x, float y) {
+    // drawing text requires GlyphID TextEncoding (which JNI layer would have done)
+    LOG_ALWAYS_FATAL_IF(paint.getTextEncoding() != SkPaint::kGlyphID_TextEncoding,
+            "must use glyph encoding");
 
-   SkMatrix identity;
-   identity.setIdentity();
-   SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry);
-   SkAutoGlyphCacheNoGamma autoCache(paint, &surfaceProps, &identity);
+    SkMatrix identity;
+    identity.setIdentity();
+    SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry);
+    SkAutoGlyphCacheNoGamma autoCache(paint, &surfaceProps, &identity);
 
-   float totalAdvance = 0;
-   std::vector<glyph_t> glyphs;
-   std::vector<float> positions;
-   Rect bounds;
-   while (*text != '\0') {
-       SkUnichar unichar = SkUTF8_NextUnichar(&text);
-       glyph_t glyph = autoCache.getCache()->unicharToGlyph(unichar);
-       autoCache.getCache()->unicharToGlyph(unichar);
+    float totalAdvance = 0;
+    std::vector<glyph_t> glyphs;
+    std::vector<float> positions;
+    Rect bounds;
+    while (*text != '\0') {
+        SkUnichar unichar = SkUTF8_NextUnichar(&text);
+        glyph_t glyph = autoCache.getCache()->unicharToGlyph(unichar);
+        autoCache.getCache()->unicharToGlyph(unichar);
 
-       // push glyph and its relative position
-       glyphs.push_back(glyph);
-       positions.push_back(totalAdvance);
-       positions.push_back(0);
+        // push glyph and its relative position
+        glyphs.push_back(glyph);
+        positions.push_back(totalAdvance);
+        positions.push_back(0);
 
-       // compute bounds
-       SkGlyph skGlyph = autoCache.getCache()->getUnicharMetrics(unichar);
-       Rect glyphBounds(skGlyph.fWidth, skGlyph.fHeight);
-       glyphBounds.translate(totalAdvance + skGlyph.fLeft, skGlyph.fTop);
-       bounds.unionWith(glyphBounds);
+        // compute bounds
+        SkGlyph skGlyph = autoCache.getCache()->getUnicharMetrics(unichar);
+        Rect glyphBounds(skGlyph.fWidth, skGlyph.fHeight);
+        glyphBounds.translate(totalAdvance + skGlyph.fLeft, skGlyph.fTop);
+        bounds.unionWith(glyphBounds);
 
-       // advance next character
-       SkScalar skWidth;
-       paint.getTextWidths(&glyph, sizeof(glyph), &skWidth, NULL);
-       totalAdvance += skWidth;
-   }
-   bounds.translate(x, y);
-   canvas->drawText(glyphs.data(), positions.data(), glyphs.size(), paint, x, y,
-           bounds.left, bounds.top, bounds.right, bounds.bottom, totalAdvance);
+        // advance next character
+        SkScalar skWidth;
+        paint.getTextWidths(&glyph, sizeof(glyph), &skWidth, NULL);
+        totalAdvance += skWidth;
+    }
+
+    // apply alignment via x parameter (which JNI layer would have done)
+    if (paint.getTextAlign() == SkPaint::kCenter_Align) {
+        x -= totalAdvance / 2;
+    } else if (paint.getTextAlign() == SkPaint::kRight_Align) {
+        x -= totalAdvance;
+    }
+
+    bounds.translate(x, y);
+
+    // Force left alignment, since alignment offset is already baked in
+    SkPaint alignPaintCopy(paint);
+    alignPaintCopy.setTextAlign(SkPaint::kLeft_Align);
+    canvas->drawText(glyphs.data(), positions.data(), glyphs.size(), alignPaintCopy, x, y,
+                bounds.left, bounds.top, bounds.right, bounds.bottom, totalAdvance);
 }
 
 } /* namespace uirenderer */
diff --git a/libs/hwui/utils/TestUtils.h b/libs/hwui/utils/TestUtils.h
index f9fa242..9c1c0b9 100644
--- a/libs/hwui/utils/TestUtils.h
+++ b/libs/hwui/utils/TestUtils.h
@@ -121,7 +121,7 @@
     }
 
     static sp<RenderNode> createNode(int left, int top, int right, int bottom,
-            std::function<void(RenderProperties& props, TestCanvas& canvas)> setup = nullptr) {
+            std::function<void(RenderProperties& props, TestCanvas& canvas)> setup) {
 #if HWUI_NULL_GPU
         // if RenderNodes are being sync'd/used, device info will be needed, since
         // DeviceInfo::maxTextureSize() affects layer property
@@ -140,22 +140,6 @@
         return node;
     }
 
-    static sp<RenderNode> createNode(int left, int top, int right, int bottom,
-            std::function<void(RenderProperties& props)> setup) {
-        return createNode(left, top, right, bottom,
-                [&setup](RenderProperties& props, TestCanvas& canvas) {
-            setup(props);
-        });
-    }
-
-    static sp<RenderNode> createNode(int left, int top, int right, int bottom,
-            std::function<void(TestCanvas& canvas)> setup) {
-        return createNode(left, top, right, bottom,
-                [&setup](RenderProperties& props, TestCanvas& canvas) {
-            setup(canvas);
-        });
-    }
-
     static void recordNode(RenderNode& node,
             std::function<void(TestCanvas&)> contentCallback) {
        TestCanvas canvas(node.stagingProperties().getWidth(),
@@ -164,6 +148,13 @@
        node.setStagingDisplayList(canvas.finishRecording());
     }
 
+    /**
+     * Forces a sync of a tree of RenderNode, such that every descendant will have its staging
+     * properties and DisplayList moved to the render copies.
+     *
+     * Note: does not check dirtiness bits, so any non-staging DisplayLists will be discarded.
+     * For this reason, this should generally only be called once on a tree.
+     */
     static void syncHierarchyPropertiesAndDisplayList(sp<RenderNode>& node) {
         syncHierarchyPropertiesAndDisplayListImpl(node.get());
     }
@@ -197,7 +188,7 @@
     static SkColor interpolateColor(float fraction, SkColor start, SkColor end);
 
     static void drawTextToCanvas(TestCanvas* canvas, const char* text,
-            const SkPaint& inPaint, float x, float y);
+            const SkPaint& paint, float x, float y);
 
 private:
     static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) {
diff --git a/packages/SystemUI/Android.mk b/packages/SystemUI/Android.mk
index 0cc2a67..ece34fb 100644
--- a/packages/SystemUI/Android.mk
+++ b/packages/SystemUI/Android.mk
@@ -6,7 +6,9 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-proto-files-under,src) $(call all-Iaidl-files-under, src) \
     src/com/android/systemui/EventLogTags.logtags
 
-LOCAL_STATIC_JAVA_LIBRARIES := Keyguard
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    Keyguard \
+    android-support-v7-recyclerview
 LOCAL_JAVA_LIBRARIES := telephony-common
 
 LOCAL_PACKAGE_NAME := SystemUI
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index f2920e5..75e7959 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -21,11 +21,6 @@
     public <init>(android.os.Bundle);
 }
 
--keep class com.android.systemui.recents.views.TaskStackLayoutAlgorithm {
-    public float getFocusState();
-    public void setFocusState(float);
-}
-
 -keep class com.android.systemui.recents.views.TaskView {
     public int getDim();
     public void setDim(int);
diff --git a/packages/SystemUI/res/layout/recents.xml b/packages/SystemUI/res/layout/recents.xml
index 064d225..2c010dd 100644
--- a/packages/SystemUI/res/layout/recents.xml
+++ b/packages/SystemUI/res/layout/recents.xml
@@ -39,6 +39,12 @@
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
 
+    <!-- History View -->
+    <ViewStub android:id="@+id/history_view_stub"
+           android:layout="@layout/recents_history"
+           android:layout_width="match_parent"
+           android:layout_height="match_parent" />
+
     <!-- Nav Bar Scrim View -->
     <ImageView
         android:id="@+id/nav_bar_scrim"
diff --git a/packages/SystemUI/res/layout/recents_history.xml b/packages/SystemUI/res/layout/recents_history.xml
new file mode 100644
index 0000000..de70d30
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_history.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<com.android.systemui.recents.history.RecentsHistoryView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#99000000"
+    android:orientation="vertical">
+    <TextView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="14dp"
+        android:gravity="start"
+        android:text="@string/recents_history_label"
+        android:textSize="24sp"
+        android:textColor="#FFFFFF"
+        android:fontFamily="sans-serif-medium" />
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</com.android.systemui.recents.history.RecentsHistoryView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/recents_history_button.xml b/packages/SystemUI/res/layout/recents_history_button.xml
new file mode 100644
index 0000000..471f518
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_history_button.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="start|center_vertical"
+    android:text="@string/recents_show_history_button_label"
+    android:textSize="14sp"
+    android:textColor="#FFFFFF"
+    android:textAllCaps="true"
+    android:fontFamily="sans-serif-medium"
+    android:visibility="invisible" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/recents_history_date.xml b/packages/SystemUI/res/layout/recents_history_date.xml
new file mode 100644
index 0000000..6d6a9ee
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_history_date.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:theme="@android:style/Theme.Material"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="12dp"
+    android:gravity="start"
+    android:textSize="14sp"
+    android:textColor="#009688"
+    android:textAllCaps="true"
+    android:fontFamily="sans-serif-medium"
+    android:background="?android:selectableItemBackground"
+    android:alpha="1" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/recents_history_task.xml b/packages/SystemUI/res/layout/recents_history_task.xml
new file mode 100644
index 0000000..b9de156
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_history_task.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:theme="@android:style/Theme.Material"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"
+    android:paddingLeft="32dp"
+    android:gravity="start|center_vertical"
+    android:textSize="14sp"
+    android:textColor="#FFFFFF"
+    android:fontFamily="sans-serif-medium"
+    android:background="?android:selectableItemBackground"
+    android:alpha="1" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index d8193ab..902db26 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -177,6 +177,9 @@
     <!-- The animation duration for scrolling the stack to a particular item. -->
     <integer name="recents_animate_task_stack_scroll_duration">225</integer>
 
+    <!-- The animation duration for entering and exiting the history. -->
+    <integer name="recents_history_transition_duration">250</integer>
+
     <!-- The minimum alpha for the dim applied to cards that go deeper into the stack. -->
     <integer name="recents_max_task_stack_view_dim">96</integer>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e0c0f6a..04233ba 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -698,6 +698,10 @@
     <string name="recents_search_bar_label">search</string>
     <!-- Recents: Launch error string. [CHAR LIMIT=NONE] -->
     <string name="recents_launch_error_message">Could not start <xliff:g id="app" example="Calendar">%s</xliff:g>.</string>
+    <!-- Recents: Show history string. [CHAR LIMIT=NONE] -->
+    <string name="recents_show_history_button_label">More</string>
+    <!-- Recents: The history of recents. [CHAR LIMIT=NONE] -->
+    <string name="recents_history_label">History</string>
 
     <!-- Recents: MultiStack add stack split horizontal radio button. [CHAR LIMIT=NONE] -->
     <string name="recents_multistack_add_stack_dialog_split_horizontal">Split Horizontal</string>
@@ -1157,21 +1161,26 @@
     <!-- Option to use new paging layout in quick settings [CHAR LIMIT=60] -->
     <string name="qs_paging" translatable="false">Use the new Quick Settings</string>
 
-    <!-- Toggles paging recents via the recents button -->
+    <!-- Toggles paging recents via the recents button. DO NOT TRANSLATE -->
     <string name="overview_page_on_toggle">Enable paging</string>
-    <!-- Description for the toggle for fast-toggling recents via the recents button -->
+    <!-- Description for the toggle for fast-toggling recents via the recents button. DO NOT TRANSLATE -->
     <string name="overview_page_on_toggle_desc">Enable paging via the Overview button</string>
 
-    <!-- Toggles fast-toggling recents via the recents button -->
+    <!-- Toggles fast-toggling recents via the recents button. DO NOT TRANSLATE -->
     <string name="overview_fast_toggle_via_button">Enable fast toggle</string>
-    <!-- Description for the toggle for fast-toggling recents via the recents button -->
+    <!-- Description for the toggle for fast-toggling recents via the recents button. DO NOT TRANSLATE -->
     <string name="overview_fast_toggle_via_button_desc">Enable launch timeout while paging</string>
 
-    <!-- Toggles fullscreen screenshots -->
+    <!-- Toggles fullscreen screenshots. DO NOT TRANSLATE -->
     <string name="overview_fullscreen_thumbnails">Enable fullscreen screenshots</string>
-    <!-- Description for the toggle for fullscreen screenshots -->
+    <!-- Description for the toggle for fullscreen screenshots. DO NOT TRANSLATE -->
     <string name="overview_fullscreen_thumbnails_desc">Enable fullscreen screenshots in Overview</string>
 
+    <!-- Toggle to show the history view in Overview. DO NOT TRANSLATE -->
+    <string name="overview_show_history">Show History</string>
+    <!-- Description for the toggle to show the history view in Overview. DO NOT TRANSLATE -->
+    <string name="overview_show_history_desc">Enables the history view to see more recent tasks</string>
+
     <!-- Category in the System UI Tuner settings, where new/experimental
          settings are -->
     <string name="experimental">Experimental</string>
diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml
index 4d07d5f..f398af3 100644
--- a/packages/SystemUI/res/xml/tuner_prefs.xml
+++ b/packages/SystemUI/res/xml/tuner_prefs.xml
@@ -102,6 +102,11 @@
             android:title="@string/overview_fullscreen_thumbnails"
             android:summary="@string/overview_fullscreen_thumbnails_desc" />
 
+        <com.android.systemui.tuner.TunerSwitch
+            android:key="overview_show_history"
+            android:title="@string/overview_show_history"
+            android:summary="@string/overview_show_history_desc" />
+
     </PreferenceScreen>
 
     <SwitchPreference
diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java
index 9d98772..7933cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/Prefs.java
+++ b/packages/SystemUI/src/com/android/systemui/Prefs.java
@@ -32,6 +32,7 @@
     @StringDef({
         Key.OVERVIEW_SEARCH_APP_WIDGET_ID,
         Key.OVERVIEW_SEARCH_APP_WIDGET_PACKAGE,
+        Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME,
         Key.DEBUG_MODE_ENABLED,
         Key.HOTSPOT_TILE_LAST_USED,
         Key.COLOR_INVERSION_TILE_LAST_USED,
@@ -46,6 +47,7 @@
     public @interface Key {
         String OVERVIEW_SEARCH_APP_WIDGET_ID = "searchAppWidgetId";
         String OVERVIEW_SEARCH_APP_WIDGET_PACKAGE = "searchAppWidgetPackage";
+        String OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME = "OverviewLastStackTaskActiveTime";
         String DEBUG_MODE_ENABLED = "debugModeEnabled";
         String HOTSPOT_TILE_LAST_USED = "HotspotTileLastUsed";
         String COLOR_INVERSION_TILE_LAST_USED = "ColorInversionTileLastUsed";
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index 16b1592..94c45a4 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -46,10 +46,12 @@
 import com.android.systemui.recents.events.activity.DebugFlagsChangedEvent;
 import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationCompletedEvent;
 import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
+import com.android.systemui.recents.events.activity.HideHistoryEvent;
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.activity.IterateRecentsEvent;
 import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
 import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
+import com.android.systemui.recents.events.activity.ShowHistoryEvent;
 import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
 import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
@@ -65,6 +67,7 @@
 import com.android.systemui.recents.events.ui.focus.DismissFocusedTaskViewEvent;
 import com.android.systemui.recents.events.ui.focus.FocusNextTaskViewEvent;
 import com.android.systemui.recents.events.ui.focus.FocusPreviousTaskViewEvent;
+import com.android.systemui.recents.history.RecentsHistoryView;
 import com.android.systemui.recents.misc.DozeTrigger;
 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
 import com.android.systemui.recents.misc.SystemServicesProxy;
@@ -99,6 +102,8 @@
     private SystemBarScrimViews mScrimViews;
     private ViewStub mEmptyViewStub;
     private View mEmptyView;
+    private ViewStub mHistoryViewStub;
+    private RecentsHistoryView mHistoryView;
 
     // Resize task debug
     private RecentsResizeTaskDialog mResizeTaskDebugDialog;
@@ -196,14 +201,12 @@
 
         TaskStack stack = plan.getTaskStack();
         launchState.launchedWithNoRecentTasks = !plan.hasTasks();
-        if (!launchState.launchedWithNoRecentTasks) {
-            mRecentsView.setTaskStack(stack);
-        }
+        mRecentsView.setTaskStack(stack);
 
         // Mark the task that is the launch target
         int launchTaskIndexInStack = 0;
         if (launchState.launchedToTaskId != -1) {
-            ArrayList<Task> tasks = stack.getTasks();
+            ArrayList<Task> tasks = stack.getStackTasks();
             int taskCount = tasks.size();
             for (int j = 0; j < taskCount; j++) {
                 Task t = tasks.get(j);
@@ -255,11 +258,23 @@
             MetricsLogger.count(this, "overview_source_home", 1);
         }
         // Keep track of the total stack task count
-        int taskCount = stack.getTaskCount();
+        int taskCount = stack.getStackTaskCount();
         MetricsLogger.histogram(this, "overview_task_count", taskCount);
     }
 
     /**
+     * Dismisses the history view back into the stack view.
+     */
+    boolean dismissHistory() {
+        // Try and hide the history view first
+        if (mHistoryView != null && mHistoryView.isVisible()) {
+            EventBus.getDefault().send(new HideHistoryEvent(true /* animate */));
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Dismisses recents if we are already visible and the intent is to toggle the recents view.
      */
     boolean dismissRecentsToFocusedTask() {
@@ -349,6 +364,7 @@
                 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
                 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
         mEmptyViewStub = (ViewStub) findViewById(R.id.empty_view_stub);
+        mHistoryViewStub = (ViewStub) findViewById(R.id.history_view_stub);
         mScrimViews = new SystemBarScrimViews(this);
         getWindow().getAttributes().privateFlags |=
                 WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
@@ -432,6 +448,9 @@
 
         // Reset some states
         mIgnoreAltTabRelease = false;
+        if (mHistoryView != null) {
+            EventBus.getDefault().send(new HideHistoryEvent(false /* animate */));
+        }
 
         // Notify that recents is now hidden
         SystemServicesProxy ssp = Recents.getSystemServices();
@@ -550,8 +569,9 @@
 
     @Override
     public void onBackPressed() {
-        // Dismiss Recents to the focused Task or Home
-        dismissRecentsToFocusedTaskOrHome();
+        if (!dismissHistory()) {
+            dismissRecentsToFocusedTaskOrHome();
+        }
     }
 
     /**** RecentsResizeTaskDialog ****/
@@ -566,7 +586,9 @@
     /**** EventBus events ****/
 
     public final void onBusEvent(ToggleRecentsEvent event) {
-        dismissRecentsToFocusedTaskOrHome();
+        if (!dismissHistory()) {
+            dismissRecentsToFocusedTaskOrHome();
+        }
     }
 
     public final void onBusEvent(IterateRecentsEvent event) {
@@ -596,7 +618,21 @@
             }
         } else if (event.triggeredFromHomeKey) {
             // Otherwise, dismiss Recents to Home
-            dismissRecentsToHome(true /* animated */);
+            if (mHistoryView != null && mHistoryView.isVisible()) {
+                ReferenceCountedTrigger t = new ReferenceCountedTrigger(this, null, null, null);
+                t.increment();
+                t.addLastDecrementRunnable(new Runnable() {
+                    @Override
+                    public void run() {
+                        dismissRecentsToHome(true /* animated */);
+                    }
+                });
+                EventBus.getDefault().send(new HideHistoryEvent(true, t));
+                t.decrement();
+
+            } else {
+                dismissRecentsToHome(true /* animated */);
+            }
         } else {
             // Do nothing
         }
@@ -726,6 +762,23 @@
         mIgnoreAltTabRelease = true;
     }
 
+    public final void onBusEvent(ShowHistoryEvent event) {
+        if (mHistoryView == null) {
+            mHistoryView = (RecentsHistoryView) mHistoryViewStub.inflate();
+            // Since this history view is inflated by a view stub after the insets have already
+            // been applied, we have to set them ourselves initial from the insets that were last
+            // provided.
+            mHistoryView.setSystemInsets(mRecentsView.getSystemInsets());
+        }
+        mHistoryView.show(mRecentsView.getTaskStack());
+    }
+
+    public final void onBusEvent(HideHistoryEvent event) {
+        if (mHistoryView != null) {
+            mHistoryView.hide(event.animate, event.postAnimationTrigger);
+        }
+    }
+
     private void refreshSearchWidgetView() {
         if (mSearchWidgetInfo != null) {
             SystemServicesProxy ssp = Recents.getSystemServices();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
index 67d7115..e8b8816 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
@@ -29,6 +29,7 @@
     private static final String KEY_FAST_TOGGLE = "overview_fast_toggle";
     private static final String KEY_PAGE_ON_TOGGLE = "overview_page_on_toggle";
     private static final String KEY_FULLSCREEN_THUMBNAILS = "overview_fullscreen_thumbnails";
+    private static final String KEY_SHOW_HISTORY = "overview_show_history";
 
     public static class Static {
         // Enables debug drawing for the transition thumbnail
@@ -52,6 +53,7 @@
     private boolean mFastToggleRecents;
     private boolean mPageOnToggle;
     private boolean mUseFullscreenThumbnails;
+    private boolean mShowHistory;
 
     /**
      * We read the prefs once when we start the activity, then update them as the tuner changes
@@ -61,7 +63,7 @@
         // Register all our flags, this will also call onTuningChanged() for each key, which will
         // initialize the current state of each flag
         TunerService.get(context).addTunable(this, KEY_FAST_TOGGLE, KEY_PAGE_ON_TOGGLE,
-                KEY_FULLSCREEN_THUMBNAILS);
+                KEY_FULLSCREEN_THUMBNAILS, KEY_SHOW_HISTORY);
     }
 
     /**
@@ -85,6 +87,13 @@
         return mUseFullscreenThumbnails;
     }
 
+    /**
+     * @return whether we should show the history
+     */
+    public boolean isHistoryEnabled() {
+        return mShowHistory;
+    }
+
     @Override
     public void onTuningChanged(String key, String newValue) {
         switch (key) {
@@ -100,6 +109,10 @@
                 mUseFullscreenThumbnails = (newValue != null) &&
                         (Integer.parseInt(newValue) != 0);
                 break;
+            case KEY_SHOW_HISTORY:
+                mShowHistory = (newValue != null) &&
+                        (Integer.parseInt(newValue) != 0);
+                break;
         }
         EventBus.getDefault().send(new DebugFlagsChangedEvent());
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index ee57863..0f82cce 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -381,7 +381,7 @@
             sInstanceLoadPlan.preloadRawTasks(topTaskHome.value);
             loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value);
             TaskStack stack = sInstanceLoadPlan.getTaskStack();
-            if (stack.getTaskCount() > 0) {
+            if (stack.getStackTaskCount() > 0) {
                 // We try and draw the thumbnail transition bitmap in parallel before
                 // toggle/show recents is called
                 preCacheThumbnailTransitionBitmapAsync(topTask, stack, mDummyStackView);
@@ -415,7 +415,7 @@
         TaskStack focusedStack = plan.getTaskStack();
 
         // Return early if there are no tasks in the focused stack
-        if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
+        if (focusedStack == null || focusedStack.getStackTaskCount() == 0) return;
 
         ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask();
         // Return early if there is no running task
@@ -423,7 +423,7 @@
 
         // Find the task in the recents list
         boolean isTopTaskHome = SystemServicesProxy.isHomeStack(runningTask.stackId);
-        ArrayList<Task> tasks = focusedStack.getTasks();
+        ArrayList<Task> tasks = focusedStack.getStackTasks();
         Task toTask = null;
         ActivityOptions launchOpts = null;
         int taskCount = tasks.size();
@@ -467,7 +467,7 @@
         TaskStack focusedStack = plan.getTaskStack();
 
         // Return early if there are no tasks in the focused stack
-        if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
+        if (focusedStack == null || focusedStack.getStackTaskCount() == 0) return;
 
         ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask();
         // Return early if there is no running task (can't determine affiliated tasks in this case)
@@ -476,7 +476,7 @@
         if (SystemServicesProxy.isHomeStack(runningTask.stackId)) return;
 
         // Find the task in the recents list
-        ArrayList<Task> tasks = focusedStack.getTasks();
+        ArrayList<Task> tasks = focusedStack.getStackTasks();
         Task toTask = null;
         ActivityOptions launchOpts = null;
         int taskCount = tasks.size();
@@ -685,10 +685,10 @@
         if (topTask.stackId == FREEFORM_WORKSPACE_STACK_ID) {
             ArrayList<AppTransitionAnimationSpec> specs = new ArrayList<>();
             stackView.getScroller().setStackScrollToInitialState();
-            ArrayList<Task> tasks = stack.getTasks();
+            ArrayList<Task> tasks = stack.getStackTasks();
             for (int i = tasks.size() - 1; i >= 0; i--) {
                 Task task = tasks.get(i);
-                if (SystemServicesProxy.isFreeformStack(task.key.stackId)) {
+                if (task.isFreeformTask()) {
                     mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
                             stackView.getScroller().getStackScroll(), mTmpTransform, null);
                     Rect toTaskRect = new Rect();
@@ -741,7 +741,7 @@
             TaskStackView stackView, int runningTaskId, Task runningTaskOut) {
         // Find the running task in the TaskStack
         Task task = null;
-        ArrayList<Task> tasks = stack.getTasks();
+        ArrayList<Task> tasks = stack.getStackTasks();
         if (runningTaskId != -1) {
             // Otherwise, try and find the task with the
             int taskCount = tasks.size();
@@ -827,7 +827,7 @@
             return;
         }
 
-        boolean hasRecentTasks = stack.getTaskCount() > 0;
+        boolean hasRecentTasks = stack.getStackTaskCount() > 0;
         boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks;
 
         if (useThumbnailTransition) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/HideHistoryButtonEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/HideHistoryButtonEvent.java
new file mode 100644
index 0000000..6c767e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/HideHistoryButtonEvent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 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.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+
+/**
+ * This is sent when the history view button should be hidden.
+ */
+public class HideHistoryButtonEvent extends EventBus.Event {
+    // Simple event
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/HideHistoryEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/HideHistoryEvent.java
new file mode 100644
index 0000000..34c35a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/HideHistoryEvent.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 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.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.misc.ReferenceCountedTrigger;
+
+/**
+ * This is sent when the history view will be closed.
+ */
+public class HideHistoryEvent extends EventBus.Event {
+
+    public final boolean animate;
+    public final ReferenceCountedTrigger postAnimationTrigger;
+
+    public HideHistoryEvent(boolean animate) {
+        this(animate, null);
+    }
+
+    public HideHistoryEvent(boolean animate, ReferenceCountedTrigger postAnimationTrigger) {
+        this.animate = animate;
+        this.postAnimationTrigger = postAnimationTrigger;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/ShowHistoryButtonEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/ShowHistoryButtonEvent.java
new file mode 100644
index 0000000..7042537
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/ShowHistoryButtonEvent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 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.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+
+/**
+ * This is sent when the history view button should be shown.
+ */
+public class ShowHistoryButtonEvent extends EventBus.Event {
+    // Simple event
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/ShowHistoryEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/ShowHistoryEvent.java
new file mode 100644
index 0000000..870119d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/ShowHistoryEvent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 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.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+
+/**
+ * This is sent when the history view button is clicked.
+ */
+public class ShowHistoryEvent extends EventBus.Event {
+    // Simple event
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
new file mode 100644
index 0000000..913d427
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2015 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.recents.history;
+
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.v7.widget.RecyclerView;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.android.systemui.R;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.TaskStack;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+
+/**
+ * An adapter for the list of recent tasks in the history view.
+ */
+public class RecentsHistoryAdapter extends RecyclerView.Adapter<RecentsHistoryAdapter.ViewHolder> {
+
+    private static final String TAG = "RecentsHistoryView";
+    private static final boolean DEBUG = false;
+
+    static final int DATE_ROW_VIEW_TYPE = 0;
+    static final int TASK_ROW_VIEW_TYPE = 1;
+
+    /**
+     * View holder implementation.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public View mContent;
+
+        public ViewHolder(View v) {
+            super(v);
+            mContent = v;
+        }
+    }
+
+    /**
+     * A single row of content.
+     */
+    private interface Row {
+        int getViewType();
+    }
+
+    /**
+     * A date row.
+     */
+    private static class DateRow implements Row {
+
+        public final String date;
+
+        public DateRow(String date) {
+            this.date = date;
+        }
+
+        @Override
+        public int getViewType() {
+            return RecentsHistoryAdapter.DATE_ROW_VIEW_TYPE;
+        }
+    }
+
+    /**
+     * A task row.
+     */
+    private static class TaskRow implements Row, View.OnClickListener {
+
+        public final String description;
+        private final int mTaskId;
+
+        public TaskRow(Task task) {
+            mTaskId = task.key.id;
+            description = task.activityLabel;
+        }
+
+        @Override
+        public void onClick(View v) {
+            SystemServicesProxy ssp = Recents.getSystemServices();
+            ssp.startActivityFromRecents(v.getContext(), mTaskId, description,
+                    ActivityOptions.makeBasic());
+        }
+
+        @Override
+        public int getViewType() {
+            return RecentsHistoryAdapter.TASK_ROW_VIEW_TYPE;
+        }
+    }
+
+    private LayoutInflater mLayoutInflater;
+    private final List<Row> mRows = new ArrayList<>();
+
+    public RecentsHistoryAdapter(Context context) {
+        mLayoutInflater = LayoutInflater.from(context);
+    }
+
+    /**
+     * Updates this adapter with the given tasks.
+     */
+    public void updateTasks(Context context, List<Task> tasks) {
+        final Locale l = context.getResources().getConfiguration().locale;
+        final String dateFormatStr = DateFormat.getBestDateTimePattern(l, "EEEEMMMMd");
+        final List<Task> tasksMostRecent = new ArrayList<>(tasks);
+        Collections.reverse(tasksMostRecent);
+        int prevDayKey = -1;
+        mRows.clear();
+        for (Task task : tasksMostRecent) {
+            if (task.isFreeformTask()) {
+                continue;
+            }
+
+            Calendar cal = Calendar.getInstance(l);
+            cal.setTimeInMillis(task.key.lastActiveTime);
+            int dayKey = Objects.hash(cal.get(Calendar.YEAR), cal.get(Calendar.DAY_OF_YEAR));
+            if (dayKey != prevDayKey) {
+                prevDayKey = dayKey;
+                mRows.add(new DateRow(DateFormat.format(dateFormatStr, cal).toString()));
+            }
+            mRows.add(new TaskRow(task));
+        }
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case DATE_ROW_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.recents_history_date, parent,
+                        false));
+            case TASK_ROW_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.recents_history_task, parent,
+                        false));
+            default:
+                return new ViewHolder(null);
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int position) {
+        Row row = mRows.get(position);
+        int viewType = mRows.get(position).getViewType();
+        switch (viewType) {
+            case DATE_ROW_VIEW_TYPE: {
+                TextView tv = (TextView) holder.mContent;
+                tv.setText(((DateRow) row).date);
+                break;
+            }
+            case TASK_ROW_VIEW_TYPE: {
+                TextView tv = (TextView) holder.mContent;
+                TaskRow taskRow = (TaskRow) row;
+                tv.setText(taskRow.description);
+                tv.setOnClickListener(taskRow);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return mRows.size();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return mRows.get(position).getViewType();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryView.java b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryView.java
new file mode 100644
index 0000000..f48883f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryView.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015 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.recents.history;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.misc.ReferenceCountedTrigger;
+import com.android.systemui.recents.model.TaskStack;
+
+/**
+ * A list of the recent tasks that are not in the stack.
+ */
+public class RecentsHistoryView extends LinearLayout {
+
+    private static final String TAG = "RecentsHistoryView";
+    private static final boolean DEBUG = false;
+
+    private RecyclerView mRecyclerView;
+    private RecentsHistoryAdapter mAdapter;
+    private boolean mIsVisible;
+
+    private Interpolator mFastOutSlowInInterpolator;
+    private Interpolator mFastOutLinearInInterpolator;
+    private int mHistoryTransitionDuration;
+
+    public RecentsHistoryView(Context context) {
+        super(context);
+    }
+
+    public RecentsHistoryView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public RecentsHistoryView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RecentsHistoryView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        Resources res = context.getResources();
+        mAdapter = new RecentsHistoryAdapter(context);
+        mHistoryTransitionDuration = res.getInteger(R.integer.recents_history_transition_duration);
+        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.fast_out_slow_in);
+        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.fast_out_linear_in);
+    }
+
+    /**
+     * Updates this history view with the recent tasks, and then shows it.
+     */
+    public void show(TaskStack stack) {
+        setVisibility(View.VISIBLE);
+        setAlpha(0f);
+        animate()
+                .alpha(1f)
+                .setDuration(mHistoryTransitionDuration)
+                .setInterpolator(mFastOutSlowInInterpolator)
+                .withLayer()
+                .start();
+
+        mAdapter.updateTasks(getContext(), stack.computeAllTasksList());
+        mIsVisible = true;
+    }
+
+    /**
+     * Hides this history view.
+     */
+    public void hide(boolean animate, final ReferenceCountedTrigger postAnimationTrigger) {
+        if (animate) {
+            animate()
+                    .alpha(0f)
+                    .setDuration(mHistoryTransitionDuration)
+                    .setInterpolator(mFastOutLinearInInterpolator)
+                    .withEndAction(new Runnable() {
+                        @Override
+                        public void run() {
+                            setVisibility(View.INVISIBLE);
+                            if (postAnimationTrigger != null) {
+                                postAnimationTrigger.decrement();
+                            }
+                        }
+                    })
+                    .withLayer()
+                    .start();
+            if (postAnimationTrigger != null) {
+                postAnimationTrigger.increment();
+            }
+        } else {
+            setAlpha(0f);
+            setVisibility(View.INVISIBLE);
+        }
+        mIsVisible = false;
+    }
+
+    /**
+     * Updates the system insets of this history view to the provided values.
+     */
+    public void setSystemInsets(Rect systemInsets) {
+        setPadding(systemInsets.left, systemInsets.top, systemInsets.right, systemInsets.bottom);
+    }
+
+    /**
+     * Returns whether this view is visible.
+     */
+    public boolean isVisible() {
+        return mIsVisible;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mRecyclerView = (RecyclerView) findViewById(R.id.list);
+        mRecyclerView.setAdapter(mAdapter);
+        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        setSystemInsets(insets.getSystemWindowInsets());
+        return insets;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
index 7b04493..5921d13 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
@@ -23,8 +23,10 @@
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
 import android.util.Log;
+import com.android.systemui.Prefs;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsConfiguration;
+import com.android.systemui.recents.RecentsDebugFlags;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 
 import java.util.ArrayList;
@@ -45,7 +47,8 @@
     private static String TAG = "RecentsTaskLoadPlan";
     private static boolean DEBUG = false;
 
-    private static int INVALID_TASK_ID = -1;
+    private static int MIN_NUM_TASKS = 5;
+    private static int SESSION_BEGIN_TIME = 60 /* s/min */ * 60 /* min/hr */ * 6 /* hrs */;
 
     /** The set of conditions to load tasks. */
     public static class Options {
@@ -99,6 +102,7 @@
     public synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) {
         if (DEBUG) Log.d(TAG, "preloadPlan");
 
+        RecentsDebugFlags debugFlags = Recents.getDebugFlags();
         RecentsConfiguration config = Recents.getConfiguration();
         SystemServicesProxy ssp = Recents.getSystemServices();
         Resources res = mContext.getResources();
@@ -107,6 +111,10 @@
         if (mRawTasks == null) {
             preloadRawTasks(isTopTaskHome);
         }
+
+        long lastStackActiveTime = Prefs.getLong(mContext,
+                Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME, 0);
+        long newLastStackActiveTime = -1;
         int taskCount = mRawTasks.size();
         for (int i = 0; i < taskCount; i++) {
             ActivityManager.RecentTaskInfo t = mRawTasks.get(i);
@@ -115,13 +123,27 @@
             Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.stackId, t.baseIntent,
                     t.userId, t.firstActiveTime, t.lastActiveTime);
 
+            // This task is only shown in the stack if it statisfies the historical time or min
+            // number of tasks constraints. Freeform tasks are also always shown.
+            boolean isStackTask = true;
+            if (debugFlags.isHistoryEnabled()) {
+                boolean isFreeformTask = SystemServicesProxy.isFreeformStack(t.stackId);
+                isStackTask = !isFreeformTask && (!isHistoricalTask(t) ||
+                        (t.lastActiveTime >= lastStackActiveTime &&
+                                i >= (taskCount - MIN_NUM_TASKS)));
+                if (isStackTask && newLastStackActiveTime < 0) {
+                    newLastStackActiveTime = t.lastActiveTime;
+                }
+            }
+
             // Load the label, icon, and color
             String activityLabel = loader.getAndUpdateActivityLabel(taskKey, t.taskDescription,
                     ssp);
             String contentDescription = loader.getAndUpdateContentDescription(taskKey,
                     activityLabel, ssp, res);
-            Drawable activityIcon = loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, ssp,
-                    res, false);
+            Drawable activityIcon = isStackTask
+                    ? loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, ssp, res, false)
+                    : null;
             int activityColor = loader.getActivityPrimaryColor(t.taskDescription);
 
             Bitmap icon = t.taskDescription != null
@@ -132,7 +154,7 @@
             // Add the task to the stack
             Task task = new Task(taskKey, t.affiliatedTaskId, t.affiliatedTaskColor, activityLabel,
                     contentDescription, activityIcon, activityColor, (i == (taskCount - 1)),
-                    config.lockToAppEnabled, icon, iconFilename, t.bounds);
+                    config.lockToAppEnabled, !isStackTask, icon, iconFilename, t.bounds);
             task.thumbnail = loader.getAndUpdateThumbnail(taskKey, ssp, false);
             if (DEBUG) {
                 Log.d(TAG, activityLabel + " bounds: " + t.bounds);
@@ -144,6 +166,10 @@
                 stackTasks.add(task);
             }
         }
+        if (debugFlags.isHistoryEnabled() && newLastStackActiveTime != -1) {
+            Prefs.putLong(mContext, Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME,
+                    newLastStackActiveTime);
+        }
 
         // Initialize the stacks
         ArrayList<Task> allTasks = new ArrayList<>();
@@ -168,7 +194,7 @@
         Resources res = mContext.getResources();
 
         // Iterate through each of the tasks and load them according to the load conditions.
-        ArrayList<Task> tasks = mStack.getTasks();
+        ArrayList<Task> tasks = mStack.getStackTasks();
         int taskCount = tasks.size();
         for (int i = 0; i < taskCount; i++) {
             ActivityManager.RecentTaskInfo t = mRawTasks.get(i);
@@ -214,8 +240,15 @@
     /** Returns whether there are any tasks in any stacks. */
     public boolean hasTasks() {
         if (mStack != null) {
-            return mStack.getTaskCount() > 0;
+            return mStack.getStackTaskCount() > 0;
         }
         return false;
     }
+
+    /**
+     * Returns whether this task is considered a task to be shown in the history.
+     */
+    private boolean isHistoricalTask(ActivityManager.RecentTaskInfo t) {
+        return t.lastActiveTime < (System.currentTimeMillis() - SESSION_BEGIN_TIME);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
index 67e18f3..60bedae 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -105,6 +105,7 @@
     public Bitmap thumbnail;
     public boolean lockToThisTask;
     public boolean lockToTaskEnabled;
+    public boolean isHistorical;
     public Bitmap icon;
     public String iconFilename;
     public Rect bounds;
@@ -117,8 +118,8 @@
 
     public Task(TaskKey key, int taskAffiliation, int taskAffiliationColor,
                 String activityTitle, String contentDescription, Drawable activityIcon,
-                int colorPrimary, boolean lockToThisTask, boolean lockToTaskEnabled, Bitmap icon,
-                String iconFilename, Rect bounds) {
+                int colorPrimary, boolean lockToThisTask, boolean lockToTaskEnabled,
+                boolean isHistorical, Bitmap icon, String iconFilename, Rect bounds) {
         boolean isInAffiliationGroup = (taskAffiliation != key.id);
         boolean hasAffiliationGroupColor = isInAffiliationGroup && (taskAffiliationColor != 0);
         this.key = key;
@@ -132,6 +133,7 @@
                 Color.WHITE) > 3f;
         this.lockToThisTask = lockToTaskEnabled && lockToThisTask;
         this.lockToTaskEnabled = lockToTaskEnabled;
+        this.isHistorical = isHistorical;
         this.icon = icon;
         this.iconFilename = iconFilename;
         this.bounds = bounds;
@@ -149,6 +151,10 @@
         this.useLightOnPrimaryColor = o.useLightOnPrimaryColor;
         this.lockToThisTask = o.lockToThisTask;
         this.lockToTaskEnabled = o.lockToTaskEnabled;
+        this.isHistorical = o.isHistorical;
+        this.icon = o.icon;
+        this.iconFilename = o.iconFilename;
+        this.bounds = o.bounds;
     }
 
     /** Set the callbacks */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 0970252..cd0a46f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -198,14 +198,14 @@
 
     /** Task stack callbacks */
     public interface TaskStackCallbacks {
-        /* Notifies when a task has been added to the stack */
-        void onStackTaskAdded(TaskStack stack, Task t);
         /* Notifies when a task has been removed from the stack */
         void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask,
                                        Task newFrontMostTask);
     }
 
-
+    /**
+     * The various possible dock states when dragging and dropping a task.
+     */
     public enum DockState implements DropTarget {
         NONE(-1, 96, null, null),
         LEFT(DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, 192,
@@ -287,10 +287,19 @@
         }
     }
 
+    // A comparator that sorts tasks by their last active time
+    private Comparator<Task> LAST_ACTIVE_TIME_COMPARATOR = new Comparator<Task>() {
+        @Override
+        public int compare(Task o1, Task o2) {
+            return Long.compare(o1.key.lastActiveTime, o2.key.lastActiveTime);
+        }
+    };
+
     // The task offset to apply to a task id as a group affiliation
     static final int IndividualTaskIdOffset = 1 << 16;
 
-    FilteredTaskList mTaskList = new FilteredTaskList();
+    FilteredTaskList mStackTaskList = new FilteredTaskList();
+    FilteredTaskList mHistoryTaskList = new FilteredTaskList();
     TaskStackCallbacks mCb;
 
     ArrayList<TaskGrouping> mGroups = new ArrayList<>();
@@ -298,10 +307,16 @@
 
     public TaskStack() {
         // Ensure that we only show non-docked tasks
-        mTaskList.setFilter(new TaskFilter() {
+        mStackTaskList.setFilter(new TaskFilter() {
             @Override
             public boolean acceptTask(Task t, int index) {
-                return !SystemServicesProxy.isDockedStack(t.key.stackId);
+                return !t.isHistorical && !SystemServicesProxy.isDockedStack(t.key.stackId);
+            }
+        });
+        mHistoryTaskList.setFilter(new TaskFilter() {
+            @Override
+            public boolean acceptTask(Task t, int index) {
+                return t.isHistorical && !SystemServicesProxy.isDockedStack(t.key.stackId);
             }
         });
     }
@@ -314,29 +329,22 @@
     /** Resets this TaskStack. */
     public void reset() {
         mCb = null;
-        mTaskList.reset();
+        mStackTaskList.reset();
+        mHistoryTaskList.reset();
         mGroups.clear();
         mAffinitiesGroups.clear();
     }
 
-    /** Adds a new task */
-    public void addTask(Task t) {
-        mTaskList.add(t);
-        if (mCb != null) {
-            mCb.onStackTaskAdded(this, t);
-        }
-    }
-
     /**
      * Moves the given task to either the front of the freeform workspace or the stack.
      */
     public void moveTaskToStack(Task task, int newStackId) {
         // Find the index to insert into
-        ArrayList<Task> taskList = mTaskList.getTasks();
+        ArrayList<Task> taskList = mStackTaskList.getTasks();
         int taskCount = taskList.size();
         if (!task.isFreeformTask() && (newStackId == FREEFORM_WORKSPACE_STACK_ID)) {
             // Insert freeform tasks at the front
-            mTaskList.moveTaskToStack(task, taskCount, newStackId);
+            mStackTaskList.moveTaskToStack(task, taskCount, newStackId);
         } else if (task.isFreeformTask() && (newStackId == FULLSCREEN_WORKSPACE_STACK_ID)) {
             // Insert after the first stacked task
             int insertIndex = 0;
@@ -346,14 +354,14 @@
                     break;
                 }
             }
-            mTaskList.moveTaskToStack(task, insertIndex, newStackId);
+            mStackTaskList.moveTaskToStack(task, insertIndex, newStackId);
         }
     }
 
     /** Does the actual work associated with removing the task. */
-    void removeTaskImpl(Task t) {
+    void removeTaskImpl(FilteredTaskList taskList, Task t) {
         // Remove the task from the list
-        mTaskList.remove(t);
+        taskList.remove(t);
         // Remove it from the group as well, and if it is empty, remove the group
         TaskGrouping group = t.group;
         group.removeTask(t);
@@ -366,10 +374,10 @@
 
     /** Removes a task */
     public void removeTask(Task t) {
-        if (mTaskList.contains(t)) {
-            boolean wasFrontMostTask = (getFrontMostTask() == t);
-            removeTaskImpl(t);
-            Task newFrontMostTask = getFrontMostTask();
+        if (mStackTaskList.contains(t)) {
+            boolean wasFrontMostTask = (getStackFrontMostTask() == t);
+            removeTaskImpl(mStackTaskList, t);
+            Task newFrontMostTask = getStackFrontMostTask();
             if (newFrontMostTask != null && newFrontMostTask.lockToTaskEnabled) {
                 newFrontMostTask.lockToThisTask = true;
             }
@@ -377,61 +385,88 @@
                 // Notify that a task has been removed
                 mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask);
             }
-        }
-    }
-
-    /** Sets a few tasks in one go */
-    public void setTasks(List<Task> tasks) {
-        ArrayList<Task> taskList = mTaskList.getTasks();
-        int taskCount = taskList.size();
-        for (int i = taskCount - 1; i >= 0; i--) {
-            Task t = taskList.get(i);
-            removeTaskImpl(t);
+        } else if (mHistoryTaskList.contains(t)) {
+            removeTaskImpl(mHistoryTaskList, t);
             if (mCb != null) {
                 // Notify that a task has been removed
                 mCb.onStackTaskRemoved(this, t, false, null);
             }
         }
-        mTaskList.set(tasks);
-        for (Task t : tasks) {
-            if (mCb != null) {
-                mCb.onStackTaskAdded(this, t);
+    }
+
+    /**
+     * Sets a few tasks in one go, without calling any callbacks.
+     */
+    public void setTasks(List<Task> tasks) {
+        ArrayList<Task> stackTasks = new ArrayList<>();
+        ArrayList<Task> historyTasks = new ArrayList<>();
+        for (Task task : tasks) {
+            if (task.isHistorical) {
+                historyTasks.add(task);
+            } else {
+                stackTasks.add(task);
             }
         }
+        mStackTaskList.set(stackTasks);
+        mHistoryTaskList.set(historyTasks);
     }
 
     /** Gets the front task */
-    public Task getFrontMostTask() {
-        if (mTaskList.size() == 0) return null;
-        return mTaskList.getTasks().get(mTaskList.size() - 1);
+    public Task getStackFrontMostTask() {
+        if (mStackTaskList.size() == 0) return null;
+        return mStackTaskList.getTasks().get(mStackTaskList.size() - 1);
     }
 
     /** Gets the task keys */
     public ArrayList<Task.TaskKey> getTaskKeys() {
         ArrayList<Task.TaskKey> taskKeys = new ArrayList<>();
-        ArrayList<Task> tasks = mTaskList.getTasks();
+        ArrayList<Task> tasks = computeAllTasksList();
         int taskCount = tasks.size();
         for (int i = 0; i < taskCount; i++) {
-            taskKeys.add(tasks.get(i).key);
+            Task task = tasks.get(i);
+            taskKeys.add(task.key);
         }
         return taskKeys;
     }
 
-    /** Gets the tasks */
-    public ArrayList<Task> getTasks() {
-        return mTaskList.getTasks();
-    }
-
-    /** Gets the number of tasks */
-    public int getTaskCount() {
-        return mTaskList.size();
+    /**
+     * Returns the set of "active" (non-historical) tasks in the stack that have been used recently.
+     */
+    public ArrayList<Task> getStackTasks() {
+        return mStackTaskList.getTasks();
     }
 
     /**
-     * Returns the task in this stack which is the launch target.
+     * Returns the set of tasks that are inactive. These tasks will be presented in a separate
+     * history view.
+     */
+    public ArrayList<Task> getHistoricalTasks() {
+        return mHistoryTaskList.getTasks();
+    }
+
+    /**
+     * Computes a set of all the active and historical tasks ordered by their last active time.
+     */
+    public ArrayList<Task> computeAllTasksList() {
+        ArrayList<Task> tasks = new ArrayList<>();
+        tasks.addAll(mStackTaskList.getTasks());
+        tasks.addAll(mHistoryTaskList.getTasks());
+        Collections.sort(tasks, LAST_ACTIVE_TIME_COMPARATOR);
+        return tasks;
+    }
+
+    /**
+     * Returns the number of tasks in the active stack.
+     */
+    public int getStackTaskCount() {
+        return mStackTaskList.size();
+    }
+
+    /**
+     * Returns the task in stack tasks which is the launch target.
      */
     public Task getLaunchTarget() {
-        ArrayList<Task> tasks = mTaskList.getTasks();
+        ArrayList<Task> tasks = mStackTaskList.getTasks();
         int taskCount = tasks.size();
         for (int i = 0; i < taskCount; i++) {
             Task task = tasks.get(i);
@@ -443,16 +478,14 @@
     }
 
     /** Returns the index of this task in this current task stack */
-    public int indexOfTask(Task t) {
-        return mTaskList.indexOf(t);
+    public int indexOfStackTask(Task t) {
+        return mStackTaskList.indexOf(t);
     }
 
     /** Finds the task with the specified task id. */
     public Task findTaskWithId(int taskId) {
-        ArrayList<Task> tasks = mTaskList.getTasks();
-        int taskCount = tasks.size();
-        for (int i = 0; i < taskCount; i++) {
-            Task task = tasks.get(i);
+        ArrayList<Task> tasks = computeAllTasksList();
+        for (Task task : tasks) {
             if (task.key.id == taskId) {
                 return task;
             }
@@ -460,21 +493,6 @@
         return null;
     }
 
-    /**
-     * Returns whether this stack has freeform tasks.
-     */
-    public boolean hasFreeformTasks() {
-        ArrayList<Task> tasks = mTaskList.getTasks();
-        int taskCount = tasks.size();
-        for (int i = 0; i < taskCount; i++) {
-            Task task = tasks.get(i);
-            if (task.isFreeformTask()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     /******** Grouping ********/
 
     /** Adds a group to the set */
@@ -500,7 +518,7 @@
         if (RecentsDebugFlags.Static.EnableSimulatedTaskGroups) {
             HashMap<Task.TaskKey, Task> taskMap = new HashMap<Task.TaskKey, Task>();
             // Sort all tasks by increasing firstActiveTime of the task
-            ArrayList<Task> tasks = mTaskList.getTasks();
+            ArrayList<Task> tasks = mStackTaskList.getTasks();
             Collections.sort(tasks, new Comparator<Task>() {
                 @Override
                 public int compare(Task task, Task task2) {
@@ -560,11 +578,11 @@
                     taskIndex++;
                 }
             }
-            mTaskList.set(tasks);
+            mStackTaskList.set(tasks);
         } else {
             // Create the task groups
             HashMap<Task.TaskKey, Task> tasksMap = new HashMap<Task.TaskKey, Task>();
-            ArrayList<Task> tasks = mTaskList.getTasks();
+            ArrayList<Task> tasks = mStackTaskList.getTasks();
             int taskCount = tasks.size();
             for (int i = 0; i < taskCount; i++) {
                 Task t = tasks.get(i);
@@ -636,8 +654,12 @@
 
     @Override
     public String toString() {
-        String str = "Tasks:\n";
-        for (Task t : mTaskList.getTasks()) {
+        String str = "Stack Tasks:\n";
+        for (Task t : mStackTaskList.getTasks()) {
+            str += "  " + t.toString() + "\n";
+        }
+        str += "Historical Tasks:\n";
+        for (Task t : mHistoryTaskList.getTasks()) {
             str += "  " + t.toString() + "\n";
         }
         return str;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
index 70370ec..945fdc1 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
@@ -16,27 +16,53 @@
 
 package com.android.systemui.recents.views;
 
+import android.animation.ObjectAnimator;
 import android.graphics.Outline;
 import android.graphics.Rect;
+import android.util.IntProperty;
+import android.util.Property;
 import android.view.View;
 import android.view.ViewOutlineProvider;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.recents.RecentsConfiguration;
 
 /* An outline provider that has a clip and outline that can be animated. */
 public class AnimateableViewBounds extends ViewOutlineProvider {
 
-    TaskView mSourceView;
+    View mSourceView;
     Rect mClipRect = new Rect();
     Rect mClipBounds = new Rect();
     int mCornerRadius;
     float mAlpha = 1f;
     final float mMinAlpha = 0.25f;
 
-    public AnimateableViewBounds(TaskView source, int cornerRadius) {
+    public static final Property<AnimateableViewBounds, Integer> CLIP_BOTTOM =
+            new IntProperty<AnimateableViewBounds>("clipBottom") {
+                @Override
+                public void setValue(AnimateableViewBounds object, int clip) {
+                    object.setClipBottom(clip, false /* force */);
+                }
+
+                @Override
+                public Integer get(AnimateableViewBounds object) {
+                    return object.getClipBottom();
+                }
+            };
+
+    public static final Property<AnimateableViewBounds, Integer> CLIP_RIGHT =
+            new IntProperty<AnimateableViewBounds>("clipRight") {
+                @Override
+                public void setValue(AnimateableViewBounds object, int clip) {
+                    object.setClipRight(clip, false /* force */);
+                }
+
+                @Override
+                public Integer get(AnimateableViewBounds object) {
+                    return object.getClipRight();
+                }
+            };
+
+    public AnimateableViewBounds(View source, int cornerRadius) {
         mSourceView = source;
         mCornerRadius = cornerRadius;
-        setClipBottom(getClipBottom(), false /* force */);
     }
 
     @Override
@@ -56,18 +82,21 @@
         }
     }
 
+    /**
+     * Animates the bottom clip.
+     */
+    public void animateClipBottom(int bottom) {
+        ObjectAnimator animator = ObjectAnimator.ofInt(this, CLIP_BOTTOM, getClipBottom(), bottom);
+        animator.setDuration(150);
+        animator.start();
+    }
+
     /** Sets the bottom clip. */
     public void setClipBottom(int bottom, boolean force) {
         if (bottom != mClipRect.bottom || force) {
             mClipRect.bottom = bottom;
             mSourceView.invalidateOutline();
             updateClipBounds();
-
-            RecentsConfiguration config = Recents.getConfiguration();
-            if (!config.useHardwareLayers) {
-                mSourceView.mThumbnailView.updateThumbnailVisibility(
-                        bottom - mSourceView.getPaddingBottom());
-            }
         }
     }
 
@@ -76,6 +105,20 @@
         return mClipRect.bottom;
     }
 
+    /** Sets the right clip. */
+    public void setClipRight(int right, boolean force) {
+        if (right != mClipRect.right || force) {
+            mClipRect.right = right;
+            mSourceView.invalidateOutline();
+            updateClipBounds();
+        }
+    }
+
+    /** Returns the right clip. */
+    public int getClipRight() {
+        return mClipRect.right;
+    }
+
     private void updateClipBounds() {
         mClipBounds.set(mClipRect.left, mClipRect.top,
                 mSourceView.getWidth() - mClipRect.right,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java
index 765f686..90d62c1 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java
@@ -110,7 +110,7 @@
             transformOut.rect.offset(transformOut.translationX, transformOut.translationY);
             Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
             transformOut.visible = true;
-            transformOut.p = 0;
+            transformOut.p = 1f;
 
             if (DEBUG) {
                 Log.d(TAG, "getTransform: " + task.key + ", " + transformOut);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
index d7eb099..736020f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
@@ -160,9 +160,9 @@
         if (ssp.startActivityFromRecents(mContext, task.key.id, task.activityLabel, opts)) {
             // Keep track of the index of the task launch
             int taskIndexFromFront = 0;
-            int taskIndex = stack.indexOfTask(task);
+            int taskIndex = stack.indexOfStackTask(task);
             if (taskIndex > -1) {
-                taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
+                taskIndexFromFront = stack.getStackTaskCount() - taskIndex - 1;
             }
             EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
         } else {
@@ -275,7 +275,7 @@
         // Otherwise, for freeform tasks, create a new animation spec for each task we have to
         // launch
         TaskStack stack = stackView.getStack();
-        ArrayList<Task> tasks = stack.getTasks();
+        ArrayList<Task> tasks = stack.getStackTasks();
         int taskCount = tasks.size();
         for (int i = taskCount - 1; i >= 0; i--) {
             Task t = tasks.get(i);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index 90456c0..0557f65 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -17,6 +17,7 @@
 package com.android.systemui.recents.views;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -25,20 +26,26 @@
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.WindowInsets;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
-import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsActivity;
 import com.android.systemui.recents.RecentsActivityLaunchState;
 import com.android.systemui.recents.RecentsAppWidgetHostView;
 import com.android.systemui.recents.RecentsConfiguration;
+import com.android.systemui.recents.RecentsDebugFlags;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
+import com.android.systemui.recents.events.activity.DebugFlagsChangedEvent;
 import com.android.systemui.recents.events.activity.DismissRecentsToHomeAnimationStarted;
+import com.android.systemui.recents.events.activity.HideHistoryButtonEvent;
+import com.android.systemui.recents.events.activity.HideHistoryEvent;
+import com.android.systemui.recents.events.activity.ShowHistoryButtonEvent;
+import com.android.systemui.recents.events.activity.ShowHistoryEvent;
 import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
 import com.android.systemui.recents.events.ui.DraggingInRecentsEndedEvent;
 import com.android.systemui.recents.events.ui.DraggingInRecentsEvent;
@@ -63,11 +70,12 @@
     private static final String TAG = "RecentsView";
     private static final boolean DEBUG = false;
 
-    LayoutInflater mInflater;
     Handler mHandler;
 
+    TaskStack mStack;
     TaskStackView mTaskStackView;
     RecentsAppWidgetHostView mSearchBar;
+    View mHistoryButton;
     boolean mAwaitingFirstLayout = true;
     boolean mLastTaskLaunchedWasFreeform;
 
@@ -81,7 +89,9 @@
             TaskStack.DockState.BOTTOM,
     };
 
-    Interpolator mFastOutSlowInInterpolator;
+    private Interpolator mFastOutSlowInInterpolator;
+    private Interpolator mFastOutLinearInInterpolator;
+    private int mHistoryTransitionDuration;
 
     Rect mSystemInsets = new Rect();
 
@@ -99,18 +109,31 @@
 
     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+        Resources res = context.getResources();
         setWillNotDraw(false);
-        mInflater = LayoutInflater.from(context);
         mHandler = new Handler();
         mTransitionHelper = new RecentsTransitionHelper(getContext(), mHandler);
         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_slow_in);
+        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.fast_out_linear_in);
+        mHistoryTransitionDuration = res.getInteger(R.integer.recents_history_transition_duration);
         mTouchHandler = new RecentsViewTouchHandler(this);
+
+        LayoutInflater inflater = LayoutInflater.from(context);
+        mHistoryButton = inflater.inflate(R.layout.recents_history_button, this, false);
+        mHistoryButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                EventBus.getDefault().send(new ShowHistoryEvent());
+            }
+        });
     }
 
     /** Set/get the bsp root node */
     public void setTaskStack(TaskStack stack) {
         RecentsConfiguration config = Recents.getConfiguration();
+        mStack = stack;
         if (config.getLaunchState().launchedReuseTaskStackViews) {
             if (mTaskStackView != null) {
                 // If onRecentsHidden is not triggered, we need to the stack view again here
@@ -129,6 +152,11 @@
             mTaskStackView.setCallbacks(this);
             addView(mTaskStackView);
         }
+        if (indexOfChild(mHistoryButton) == -1) {
+            addView(mHistoryButton);
+        } else {
+            mHistoryButton.bringToFront();
+        }
 
         // Trigger a new layout
         requestLayout();
@@ -141,13 +169,20 @@
         return mLastTaskLaunchedWasFreeform;
     }
 
+    /**
+     * Returns the currently set task stack.
+     */
+    public TaskStack getTaskStack() {
+        return mStack;
+    }
+
     /** Gets the next task in the stack - or if the last - the top task */
     public Task getNextTaskOrTopTask(Task taskToSearch) {
         Task returnTask = null;
         boolean found = false;
         if (mTaskStackView != null) {
             TaskStack stack = mTaskStackView.getStack();
-            ArrayList<Task> taskList = stack.getTasks();
+            ArrayList<Task> taskList = stack.getStackTasks();
             // Iterate the stack views and try and find the focused task
             for (int j = taskList.size() - 1; j >= 0; --j) {
                 Task task = taskList.get(j);
@@ -219,10 +254,15 @@
         }
         ctx.postAnimationTrigger.decrement();
 
+        // Hide the history button
+        int taskViewExitToHomeDuration = getResources().getInteger(
+                R.integer.recents_task_exit_to_home_duration);
+        hideHistoryButton(taskViewExitToHomeDuration);
+
         // If we are going home, cancel the previous task's window transition
         EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(null));
 
-        // Notify of the exit animation
+        // Notify sof the exit animation
         EventBus.getDefault().send(new DismissRecentsToHomeAnimationStarted());
     }
 
@@ -253,6 +293,13 @@
         }
     }
 
+    /**
+     * Returns the last known system insets.
+     */
+    public Rect getSystemInsets() {
+        return mSystemInsets;
+    }
+
     @Override
     protected void onAttachedToWindow() {
         EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1);
@@ -301,6 +348,13 @@
                     MeasureSpec.makeMeasureSpec(taskRect.height(), MeasureSpec.AT_MOST));
         }
 
+        // Measure the history button with the full space above the stack, but width-constrained
+        // to the stack
+        Rect historyButtonRect = mTaskStackView.mLayoutAlgorithm.mHistoryButtonRect;
+        measureChild(mHistoryButton,
+                MeasureSpec.makeMeasureSpec(historyButtonRect.width(), MeasureSpec.EXACTLY),
+                MeasureSpec.makeMeasureSpec(historyButtonRect.height(),
+                        MeasureSpec.EXACTLY));
         setMeasuredDimension(width, height);
     }
 
@@ -330,6 +384,12 @@
                     top + mDragView.getMeasuredHeight());
         }
 
+        // Layout the history button left-aligned with the stack, but offset from the top of the
+        // view
+        Rect historyButtonRect = mTaskStackView.mLayoutAlgorithm.mHistoryButtonRect;
+        mHistoryButton.layout(historyButtonRect.left, historyButtonRect.top,
+                historyButtonRect.right, historyButtonRect.bottom);
+
         if (mAwaitingFirstLayout) {
             mAwaitingFirstLayout = false;
 
@@ -346,7 +406,7 @@
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mSystemInsets.set(insets.getSystemWindowInsets());
         requestLayout();
-        return insets.consumeSystemWindowInsets();
+        return insets;
     }
 
     @Override
@@ -393,7 +453,7 @@
     public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv,
             final TaskStack stack, final Task task, final boolean lockToTask,
             final Rect bounds, int destinationStack) {
-        mLastTaskLaunchedWasFreeform = SystemServicesProxy.isFreeformStack(task.key.stackId);
+        mLastTaskLaunchedWasFreeform = task.isFreeformTask();
         mTransitionHelper.launchTaskFromRecents(stack, task, stackView, tv, lockToTask, bounds,
                 destinationStack);
     }
@@ -492,6 +552,67 @@
         animate().translationY(0f);
     }
 
+    public final void onBusEvent(ShowHistoryEvent event) {
+        // Hide the history button when the history view is shown
+        hideHistoryButton(mHistoryTransitionDuration);
+    }
+
+    public final void onBusEvent(HideHistoryEvent event) {
+        // Show the history button when the history view is hidden
+        showHistoryButton(mHistoryTransitionDuration);
+    }
+
+    public final void onBusEvent(ShowHistoryButtonEvent event) {
+        showHistoryButton(150);
+    }
+
+    public final void onBusEvent(HideHistoryButtonEvent event) {
+        hideHistoryButton(100);
+    }
+
+    public final void onBusEvent(DebugFlagsChangedEvent event) {
+        RecentsDebugFlags debugFlags = Recents.getDebugFlags();
+        if (!debugFlags.isHistoryEnabled()) {
+            hideHistoryButton(100);
+        }
+    }
+
+    /**
+     * Shows the history button.
+     */
+    private void showHistoryButton(int duration) {
+        RecentsDebugFlags debugFlags = Recents.getDebugFlags();
+        if (!debugFlags.isHistoryEnabled()) {
+            return;
+        }
+
+        mHistoryButton.setVisibility(View.VISIBLE);
+        mHistoryButton.animate()
+                .alpha(1f)
+                .setDuration(duration)
+                .setInterpolator(mFastOutSlowInInterpolator)
+                .withLayer()
+                .start();
+    }
+
+    /**
+     * Hides the history button.
+     */
+    private void hideHistoryButton(int duration) {
+        mHistoryButton.animate()
+                .alpha(0f)
+                .setDuration(duration)
+                .setInterpolator(mFastOutLinearInInterpolator)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        mHistoryButton.setVisibility(View.INVISIBLE);
+                    }
+                })
+                .withLayer()
+                .start();
+    }
+
     /**
      * Updates the dock region to match the specified dock state.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index 5d091b1..6af2ada 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -162,6 +162,8 @@
     public Rect mCurrentStackRect = new Rect();
     // This is the current system insets
     public Rect mSystemInsets = new Rect();
+    // This is the bounds of the history button above the stack rect
+    public Rect mHistoryButtonRect = new Rect();
 
     // The visible ranges when the stack is focused and unfocused
     private Range mUnfocusedRange;
@@ -170,7 +172,12 @@
     // The offset from the top when scrolled to the top of the stack
     private int mFocusedPeekHeight;
 
-    // The offset from the bottom of the stack to the bottom of the bounds
+    // The offset from the top of the stack to the top of the bounds when the stack is scrolled to
+    // the end
+    private int mStackTopOffset;
+
+    // The offset from the bottom of the stack to the bottom of the bounds when the stack is
+    // scrolled to the front
     private int mStackBottomOffset;
 
     // The paths defining the motion of the tasks when the stack is focused and unfocused
@@ -211,6 +218,7 @@
     FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
 
     public TaskStackLayoutAlgorithm(Context context, TaskStackView stackView) {
+        SystemServicesProxy ssp = Recents.getSystemServices();
         Resources res = context.getResources();
         mStackView = stackView;
 
@@ -276,6 +284,7 @@
 
         // The freeform height is the visible height (not including system insets) - padding above
         // freeform and below stack - gap between the freeform and stack
+        mStackTopOffset = mFocusedPeekHeight + heightPadding;
         mStackBottomOffset = mSystemInsets.bottom + heightPadding;
         int ffHeight = (taskStackBounds.height() - 2 * heightPadding - mStackBottomOffset) / 2;
         mFreeformRect.set(taskStackBounds.left + widthPadding,
@@ -290,15 +299,20 @@
                 taskStackBounds.top + heightPadding,
                 taskStackBounds.right - widthPadding,
                 taskStackBounds.bottom);
+        mCurrentStackRect = ssp.hasFreeformWorkspaceSupport() ? mFreeformStackRect : mStackRect;
+        mHistoryButtonRect.set(mCurrentStackRect.left, mCurrentStackRect.top - heightPadding,
+                mCurrentStackRect.right, mCurrentStackRect.top + mFocusedPeekHeight);
 
         // Anchor the task rect to the top-center of the non-freeform stack rect
         float aspect = (float) (taskStackBounds.width() - mSystemInsets.left - mSystemInsets.right)
                 / (taskStackBounds.height() - mSystemInsets.bottom);
         int width = mStackRect.width();
-        int height = debugFlags.isFullscreenThumbnailsEnabled() ? (int) (width / aspect) : width;
+        int minHeight = mCurrentStackRect.height() - mFocusedPeekHeight - mStackBottomOffset;
+        int height = debugFlags.isFullscreenThumbnailsEnabled()
+                ? (int) Math.min(width / aspect, minHeight)
+                : width;
         mTaskRect.set(mStackRect.left, mStackRect.top,
                 mStackRect.left + width, mStackRect.top + height);
-        mCurrentStackRect = ssp.hasFreeformWorkspaceSupport() ? mFreeformStackRect : mStackRect;
 
         // Short circuit here if the stack rects haven't changed so we don't do all the work below
         if (lastStackRect.equals(mCurrentStackRect)) {
@@ -333,7 +347,7 @@
         mTaskIndexMap.clear();
 
         // Return early if we have no tasks
-        ArrayList<Task> tasks = stack.getTasks();
+        ArrayList<Task> tasks = stack.getStackTasks();
         if (tasks.isEmpty()) {
             mFrontMostTaskP = 0;
             mMinScrollP = mMaxScrollP = 0;
@@ -573,7 +587,7 @@
                     (mCurrentStackRect.height() - mTaskRect.height()) / 2;
             y = centerYOffset + getYForDeltaP(p, 0);
             z = mMaxTranslationZ;
-            relP = p;
+            relP = 1f;
 
         } else {
             // Otherwise, update the task to the stack layout
@@ -638,17 +652,13 @@
      * Creates a new path for the focused curve.
      */
     private Path constructFocusedCurve() {
-        SystemServicesProxy ssp = Recents.getSystemServices();
         int taskBarHeight = mContext.getResources().getDimensionPixelSize(
                 R.dimen.recents_task_bar_height);
 
         // Initialize the focused curve. This curve is a piecewise curve composed of several
         // quadradic beziers that goes from (0,1) through (0.5, peek height offset),
         // (0.667, next task offset), (0.833, bottom task offset), and (1,0).
-        float peekHeightPct = 0f;
-        if (!ssp.hasFreeformWorkspaceSupport()) {
-            peekHeightPct = (float) mFocusedPeekHeight / mCurrentStackRect.height();
-        }
+        float peekHeightPct = (float) mFocusedPeekHeight / mCurrentStackRect.height();
         Path p = new Path();
         p.moveTo(0f, 1f);
         p.lineTo(0.5f, 1f - peekHeightPct);
@@ -662,8 +672,6 @@
      * Creates a new path for the unfocused curve.
      */
     private Path constructUnfocusedCurve() {
-        SystemServicesProxy ssp = Recents.getSystemServices();
-
         // Initialize the unfocused curve. This curve is a piecewise curve composed of two quadradic
         // beziers that goes from (0,1) through (0.5, peek height offset) and ends at (1,0).  This
         // ensures that we match the range, at which 0.5 represents the stack scroll at the current
@@ -672,10 +680,7 @@
         // there is a tangent at (0.5, peek height offset).
         float cpoint1X = 0.4f;
         float cpoint1Y = 1f;
-        float peekHeightPct = 0f;
-        if (!ssp.hasFreeformWorkspaceSupport()) {
-            peekHeightPct = (float) mFocusedPeekHeight / mCurrentStackRect.height();
-        }
+        float peekHeightPct = (float) mFocusedPeekHeight / mCurrentStackRect.height();
         float slope = ((1f - peekHeightPct) - cpoint1Y) / (0.5f - cpoint1X);
         float b = 1f - slope * cpoint1X;
         float cpoint2X = 0.75f;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 67710bf..30efd5f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -42,8 +42,12 @@
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
 import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationCompletedEvent;
+import com.android.systemui.recents.events.activity.HideHistoryButtonEvent;
+import com.android.systemui.recents.events.activity.HideHistoryEvent;
 import com.android.systemui.recents.events.activity.IterateRecentsEvent;
 import com.android.systemui.recents.events.activity.PackagesChangedEvent;
+import com.android.systemui.recents.events.activity.ShowHistoryButtonEvent;
+import com.android.systemui.recents.events.activity.ShowHistoryEvent;
 import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
 import com.android.systemui.recents.events.ui.AllTaskViewsDismissedEvent;
 import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
@@ -82,6 +86,10 @@
     private final static String TAG = "TaskStackView";
     private final static boolean DEBUG = false;
 
+    // The thresholds at which to show/hide the history button.
+    private static final float SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f;
+    private static final float HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f;
+
     /** The TaskView callbacks */
     interface TaskStackViewCallbacks {
         public void onTaskViewClicked(TaskStackView stackView, TaskView tv, TaskStack stack, Task t,
@@ -404,7 +412,7 @@
     boolean synchronizeStackViewsWithModel() {
         if (mStackViewsDirty) {
             // Get all the task transforms
-            ArrayList<Task> tasks = mStack.getTasks();
+            ArrayList<Task> tasks = mStack.getStackTasks();
             float stackScroll = mStackScroller.getStackScroll();
             int[] visibleStackRange = mTmpVisibleRange;
             boolean isValidVisibleStackRange = updateStackTransforms(mCurrentTaskTransforms, tasks,
@@ -424,7 +432,7 @@
             for (int i = taskViewCount - 1; i >= 0; i--) {
                 TaskView tv = taskViews.get(i);
                 Task task = tv.getTask();
-                int taskIndex = mStack.indexOfTask(task);
+                int taskIndex = mStack.indexOfStackTask(task);
                 if (task.isFreeformTask() ||
                         visibleStackRange[1] <= taskIndex && taskIndex <= visibleStackRange[0]) {
                     mTmpTaskViewMap.put(task, tv);
@@ -443,7 +451,7 @@
 
             // Pick up all the freeform tasks
             int firstVisStackIndex = isValidVisibleStackRange ? visibleStackRange[0] : 0;
-            for (int i = mStack.getTaskCount() - 1; i >= firstVisStackIndex; i--) {
+            for (int i = mStack.getStackTaskCount() - 1; i >= firstVisStackIndex; i--) {
                 Task task = tasks.get(i);
                 if (!task.isFreeformTask()) {
                     continue;
@@ -468,12 +476,12 @@
                 // Reattach it in the right z order
                 detachViewFromParent(tv);
                 int insertIndex = -1;
-                int taskIndex = mStack.indexOfTask(task);
+                int taskIndex = mStack.indexOfStackTask(task);
                 taskViews = getTaskViews();
                 taskViewCount = taskViews.size();
                 for (int j = 0; j < taskViewCount; j++) {
                     Task tvTask = taskViews.get(j).getTask();
-                    if (taskIndex < mStack.indexOfTask(tvTask)) {
+                    if (taskIndex < mStack.indexOfStackTask(tvTask)) {
                         insertIndex = j;
                         break;
                     }
@@ -543,6 +551,8 @@
      * Updates the clip for each of the task views from back to front.
      */
     void clipTaskViews(boolean forceUpdate) {
+        RecentsConfiguration config = Recents.getConfiguration();
+
         // Update the clip on each task child
         List<TaskView> taskViews = getTaskViews();
         TaskView tmpTv = null;
@@ -580,6 +590,9 @@
                 }
             }
             tv.getViewBounds().setClipBottom(clipBottom, forceUpdate);
+            if (!config.useHardwareLayers) {
+                tv.mThumbnailView.updateThumbnailVisibility(clipBottom - tv.getPaddingBottom());
+            }
         }
         mStackViewsClipDirty = false;
     }
@@ -625,14 +638,14 @@
     private boolean setFocusedTask(int taskIndex, boolean scrollToTask, final boolean animated,
                                 final boolean requestViewFocus) {
         // Find the next task to focus
-        int newFocusedTaskIndex = mStack.getTaskCount() > 0 ?
-                Math.max(0, Math.min(mStack.getTaskCount() - 1, taskIndex)) : -1;
+        int newFocusedTaskIndex = mStack.getStackTaskCount() > 0 ?
+                Math.max(0, Math.min(mStack.getStackTaskCount() - 1, taskIndex)) : -1;
         final Task newFocusedTask = (newFocusedTaskIndex != -1) ?
-                mStack.getTasks().get(newFocusedTaskIndex) : null;
+                mStack.getStackTasks().get(newFocusedTaskIndex) : null;
 
         // Reset the last focused task state if changed
         if (mFocusedTaskIndex != -1) {
-            Task focusedTask = mStack.getTasks().get(mFocusedTaskIndex);
+            Task focusedTask = mStack.getStackTasks().get(mFocusedTaskIndex);
             if (focusedTask != newFocusedTask) {
                 resetFocusedTask();
             }
@@ -713,14 +726,14 @@
         int newIndex = -1;
         if (mFocusedTaskIndex != -1) {
             if (stackTasksOnly) {
-                List<Task> tasks =  mStack.getTasks();
+                List<Task> tasks =  mStack.getStackTasks();
                 newIndex = mFocusedTaskIndex;
                 Task task = tasks.get(mFocusedTaskIndex);
                 if (task.isFreeformTask()) {
                     // Try and focus the front most stack task
                     TaskView tv = getFrontMostTaskView(stackTasksOnly);
                     if (tv != null) {
-                        newIndex = mStack.indexOfTask(tv.getTask());
+                        newIndex = mStack.indexOfStackTask(tv.getTask());
                     }
                 } else {
                     // Try the next task if it is a stack task
@@ -735,14 +748,14 @@
             } else {
                 // No restrictions, lets just move to the new task (looping forward/backwards if
                 // necessary)
-                int taskCount = mStack.getTaskCount();
+                int taskCount = mStack.getStackTaskCount();
                 newIndex = (mFocusedTaskIndex + (forward ? -1 : 1) + taskCount) % taskCount;
             }
         } else {
             // We don't have a focused task, so focus the first visible task view
             TaskView tv = getFrontMostTaskView(stackTasksOnly);
             if (tv != null) {
-                newIndex = mStack.indexOfTask(tv.getTask());
+                newIndex = mStack.indexOfStackTask(tv.getTask());
             }
         }
         if (newIndex != -1) {
@@ -760,7 +773,7 @@
      */
     void resetFocusedTask() {
         if (mFocusedTaskIndex != -1) {
-            Task t = mStack.getTasks().get(mFocusedTaskIndex);
+            Task t = mStack.getStackTasks().get(mFocusedTaskIndex);
             TaskView tv = getChildViewForTask(t);
             if (tv != null) {
                 tv.setFocusedState(false, false /* animated */, false /* requestViewFocus */);
@@ -774,7 +787,7 @@
      */
     Task getFocusedTask() {
         if (mFocusedTaskIndex != -1) {
-            return mStack.getTasks().get(mFocusedTaskIndex);
+            return mStack.getStackTasks().get(mFocusedTaskIndex);
         }
         return null;
     }
@@ -787,11 +800,11 @@
         if (taskViewCount > 0) {
             TaskView backMostTask = taskViews.get(0);
             TaskView frontMostTask = taskViews.get(taskViewCount - 1);
-            event.setFromIndex(mStack.indexOfTask(backMostTask.getTask()));
-            event.setToIndex(mStack.indexOfTask(frontMostTask.getTask()));
+            event.setFromIndex(mStack.indexOfStackTask(backMostTask.getTask()));
+            event.setToIndex(mStack.indexOfStackTask(frontMostTask.getTask()));
             event.setContentDescription(frontMostTask.getTask().activityLabel);
         }
-        event.setItemCount(mStack.getTaskCount());
+        event.setItemCount(mStack.getStackTaskCount());
         event.setScrollY(mStackScroller.mScroller.getCurrY());
         event.setMaxScrollY(mStackScroller.progressToScrollRange(mLayoutAlgorithm.mMaxScrollP));
     }
@@ -806,7 +819,7 @@
             if (mFocusedTaskIndex > 0) {
                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
             }
-            if (mFocusedTaskIndex < mStack.getTaskCount() - 1) {
+            if (mFocusedTaskIndex < mStack.getStackTaskCount() - 1) {
                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
             }
         }
@@ -883,7 +896,7 @@
      * updateLayoutForStack() is called first.
      */
     public TaskStackLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() {
-        return mLayoutAlgorithm.computeStackVisibilityReport(mStack.getTasks());
+        return mLayoutAlgorithm.computeStackVisibilityReport(mStack.getStackTasks());
     }
 
     public void setTaskStackBounds(Rect taskStackBounds, Rect systemInsets) {
@@ -1011,12 +1024,17 @@
         // until after the enter-animation
         RecentsConfiguration config = Recents.getConfiguration();
         RecentsActivityLaunchState launchState = config.getLaunchState();
-        int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount());
+        int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getStackTaskCount());
         if (focusedTaskIndex != -1) {
             setFocusedTask(focusedTaskIndex, false /* scrollToTask */, false /* animated */,
                     false /* requestViewFocus */);
         }
 
+        // Update the history button visibility
+        if (mStackScroller.getStackScroll() < SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD) {
+            EventBus.getDefault().send(new ShowHistoryButtonEvent());
+        }
+
         // Start dozing
         mUIDozeTrigger.startDozing();
     }
@@ -1030,7 +1048,7 @@
             return;
         }
 
-        if (mStack.getTaskCount() > 0) {
+        if (mStack.getStackTaskCount() > 0) {
             // Find the launch target task
             Task launchTargetTask = mStack.getLaunchTarget();
             List<TaskView> taskViews = getTaskViews();
@@ -1139,7 +1157,7 @@
      * Launches the freeform tasks.
      */
     public boolean launchFreeformTasks() {
-        Task frontTask = mStack.getFrontMostTask();
+        Task frontTask = mStack.getStackFrontMostTask();
         if (frontTask != null && frontTask.isFreeformTask()) {
             onTaskViewClicked(getChildViewForTask(frontTask), frontTask, false);
             return true;
@@ -1150,11 +1168,6 @@
     /**** TaskStackCallbacks Implementation ****/
 
     @Override
-    public void onStackTaskAdded(TaskStack stack, Task t) {
-        requestSynchronizeStackViewsWithModel();
-    }
-
-    @Override
     public void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask,
             Task newFrontMostTask) {
         if (!removedTask.isFreeformTask()) {
@@ -1169,9 +1182,9 @@
             // front most task will be our anchor task)
             Task anchorTask = null;
             float prevAnchorTaskScroll = 0;
-            boolean pullStackForward = stack.getTaskCount() > 0;
+            boolean pullStackForward = stack.getStackTaskCount() > 0;
             if (pullStackForward) {
-                anchorTask = mStack.getFrontMostTask();
+                anchorTask = mStack.getStackFrontMostTask();
                 prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
             }
 
@@ -1224,8 +1237,8 @@
         }
 
         // If there are no remaining tasks, then just close recents
-        if (mStack.getTaskCount() == 0) {
-            boolean shouldFinishActivity = (mStack.getTaskCount() == 0);
+        if (mStack.getStackTaskCount() == 0) {
+            boolean shouldFinishActivity = (mStack.getStackTaskCount() == 0);
             if (shouldFinishActivity) {
                 EventBus.getDefault().send(new AllTaskViewsDismissedEvent());
             }
@@ -1275,13 +1288,13 @@
 
         // Find the index where this task should be placed in the stack
         int insertIndex = -1;
-        int taskIndex = mStack.indexOfTask(task);
+        int taskIndex = mStack.indexOfStackTask(task);
         if (taskIndex != -1) {
             List<TaskView> taskViews = getTaskViews();
             int taskViewCount = taskViews.size();
             for (int i = 0; i < taskViewCount; i++) {
                 Task tvTask = taskViews.get(i).getTask();
-                if (taskIndex < mStack.indexOfTask(tvTask)) {
+                if (taskIndex < mStack.indexOfStackTask(tvTask)) {
                     insertIndex = i;
                     break;
                 }
@@ -1333,10 +1346,18 @@
     /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/
 
     @Override
-    public void onScrollChanged(float p) {
+    public void onScrollChanged(float prevScroll, float curScroll) {
         mUIDozeTrigger.poke();
         requestSynchronizeStackViewsWithModel();
         postInvalidateOnAnimation();
+
+        if (prevScroll > SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD &&
+                curScroll <= SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD) {
+            EventBus.getDefault().send(new ShowHistoryButtonEvent());
+        } else if (prevScroll < HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD &&
+                curScroll >= HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD) {
+            EventBus.getDefault().send(new HideHistoryButtonEvent());
+        }
     }
 
     /**** EventBus Events ****/
@@ -1347,7 +1368,7 @@
                 event.packageName, event.userId);
 
         // For other tasks, just remove them directly if they no longer exist
-        ArrayList<Task> tasks = mStack.getTasks();
+        ArrayList<Task> tasks = mStack.getStackTasks();
         for (int i = tasks.size() - 1; i >= 0; i--) {
             final Task t = tasks.get(i);
             if (removedComponents.contains(t.key.getComponent())) {
@@ -1382,7 +1403,7 @@
 
     public final void onBusEvent(DismissFocusedTaskViewEvent event) {
         if (mFocusedTaskIndex != -1) {
-            Task t = mStack.getTasks().get(mFocusedTaskIndex);
+            Task t = mStack.getStackTasks().get(mFocusedTaskIndex);
             TaskView tv = getChildViewForTask(t);
             tv.dismissTask();
         }
@@ -1496,15 +1517,30 @@
         }
     }
 
+    public final void onBusEvent(ShowHistoryEvent event) {
+        List<TaskView> taskViews = getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = taskViewCount - 1; i >= 0; i--) {
+            TaskView tv = taskViews.get(i);
+            tv.animate().alpha(0f).setDuration(200).start();
+        }
+    }
+
+    public final void onBusEvent(HideHistoryEvent event) {
+        List<TaskView> taskViews = getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = taskViewCount - 1; i >= 0; i--) {
+            TaskView tv = taskViews.get(i);
+            tv.animate().alpha(1f).setDuration(200).start();
+        }
+    }
+
     /**
      * Removes the task from the stack, and updates the focus to the next task in the stack if the
      * removed TaskView was focused.
      */
     private void removeTaskViewFromStack(TaskView tv) {
-        SystemServicesProxy ssp = Recents.getSystemServices();
         Task task = tv.getTask();
-        int taskIndex = mStack.indexOfTask(task);
-        boolean taskWasFocused = tv.isFocusedTask();
 
         // Reset the previously focused task before it is removed from the stack
         resetFocusedTask();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
index 62b640e..90b73fe 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
@@ -35,7 +35,7 @@
     private static final boolean DEBUG = false;
 
     public interface TaskStackViewScrollerCallbacks {
-        void onScrollChanged(float p);
+        void onScrollChanged(float prevScroll, float curScroll);
     }
 
     Context mContext;
@@ -78,9 +78,10 @@
 
     /** Sets the current stack scroll */
     public void setStackScroll(float s) {
+        float prevStackScroll = mStackScrollP;
         mStackScrollP = s;
         if (mCb != null) {
-            mCb.onScrollChanged(mStackScrollP);
+            mCb.onScrollChanged(prevStackScroll, mStackScrollP);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 523f84f..6db2eb9 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -71,7 +71,7 @@
     ObjectAnimator mTaskProgressAnimator;
     float mMaxDimScale;
     int mDimAlpha;
-    AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(1f);
+    AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(3f);
     PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
     Paint mDimLayerPaint = new Paint();
     float mActionButtonTranslationZ;
@@ -265,15 +265,6 @@
         toTransform.translationZ = 0;
     }
 
-    /**
-     * When we are un/filtering, this method will setup the transform that we are animating from,
-     * in order to show the task.
-     */
-    void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) {
-        // Fade the view in
-        fromTransform.alpha = 0f;
-    }
-
     /** Prepares this task view for the enter-recents animations.  This is called earlier in the
      * first layout because the actual animation into recents may take a long time. */
     void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, boolean hideTask,
@@ -617,12 +608,11 @@
 
     /** Compute the dim as a function of the scale of this view. */
     int getDimFromTaskProgress() {
-        // TODO: Temporarily disable the dim on the stack
-        /*
-        float dim = mMaxDimScale * mDimInterpolator.getInterpolation(1f - mTaskProgress);
+        float x = mTaskProgress < 0
+                ? 1f
+                : mDimInterpolator.getInterpolation(1f - mTaskProgress);
+        float dim = mMaxDimScale * x;
         return (int) (dim * 255);
-        */
-        return 0;
     }
 
     /** Update the dim as a function of the scale of this view. */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
index bc50846..b3d263e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
@@ -45,6 +45,8 @@
  */
 public class TaskViewThumbnail extends View {
 
+    private Task mTask;
+
     // Drawing
     int mCornerRadius;
     float mDimAlpha;
@@ -52,6 +54,7 @@
     Paint mDrawPaint = new Paint();
     RectF mBitmapRect = new RectF();
     RectF mLayoutRect = new RectF();
+    float mBitmapScale = 1f;
     BitmapShader mBitmapShader;
     LightingColorFilter mLightingColorFilter = new LightingColorFilter(0xffffffff, 0);
 
@@ -165,7 +168,16 @@
     /** Updates the thumbnail shader's scale transform. */
     void updateThumbnailScale() {
         if (mBitmapShader != null) {
-            mScaleMatrix.setRectToRect(mBitmapRect, mLayoutRect, Matrix.ScaleToFit.FILL);
+            if (mTask.isFreeformTask()) {
+                // For freeform tasks, we scale the bitmap rect to fit in the layout rect
+                mBitmapScale = Math.min(mLayoutRect.width() / mBitmapRect.width(),
+                        mLayoutRect.height() / mBitmapRect.height());
+            } else {
+                // For stack tasks, we scale the bitmap to fit the width
+                mBitmapScale = Math.max(1f, mLayoutRect.width() / mBitmapRect.width());
+            }
+
+            mScaleMatrix.setScale(mBitmapScale, mBitmapScale);
             mBitmapShader.setLocalMatrix(mScaleMatrix);
         }
     }
@@ -202,6 +214,7 @@
 
     /** Binds the thumbnail view to the task */
     void rebindToTask(Task t) {
+        mTask = t;
         if (t.thumbnail != null) {
             setThumbnail(t.thumbnail);
         } else {
@@ -211,6 +224,7 @@
 
     /** Unbinds the thumbnail view from the task */
     void unbindFromTask() {
+        mTask = null;
         setThumbnail(null);
     }
 
diff --git a/packages/SystemUI/tests/Android.mk b/packages/SystemUI/tests/Android.mk
index fdc2543..9cf64d3 100644
--- a/packages/SystemUI/tests/Android.mk
+++ b/packages/SystemUI/tests/Android.mk
@@ -37,7 +37,10 @@
 
 LOCAL_PACKAGE_NAME := SystemUITests
 
-LOCAL_STATIC_JAVA_LIBRARIES := mockito-target Keyguard
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    mockito-target \
+    Keyguard \
+    android-support-v7-recyclerview
 
 # sign this with platform cert, so this test is allowed to inject key events into
 # UI it doesn't own. This is necessary to allow screenshots to be taken
diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
index 5e4f2b2..78a4e35 100644
--- a/services/core/java/com/android/server/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -1225,7 +1225,7 @@
     void unbindCurrentClientLocked(
             /* @InputMethodClient.UnbindReason */ final int unbindClientReason) {
         if (mCurClient != null) {
-            if (DEBUG) Slog.v(TAG, "unbindCurrentInputLocked: client = "
+            if (DEBUG) Slog.v(TAG, "unbindCurrentInputLocked: client="
                     + mCurClient.client.asBinder());
             if (mBoundToMethod) {
                 mBoundToMethod = false;
@@ -1290,8 +1290,10 @@
                 mCurId, mCurSeq, mCurUserActionNotificationSequenceNumber);
     }
 
-    InputBindResult startInputLocked(IInputMethodClient client,
-            IInputContext inputContext, EditorInfo attribute, int controlFlags) {
+    InputBindResult startInputLocked(
+            /* @InputMethodClient.StartInputReason */ final int startInputReason,
+            IInputMethodClient client, IInputContext inputContext, EditorInfo attribute,
+            int controlFlags) {
         // If no method is currently selected, do nothing.
         if (mCurMethodId == null) {
             return mNoBinding;
@@ -1320,8 +1322,8 @@
         return startInputUncheckedLocked(cs, inputContext, attribute, controlFlags);
     }
 
-    InputBindResult startInputUncheckedLocked(@NonNull ClientState cs,
-            IInputContext inputContext, @NonNull EditorInfo attribute, int controlFlags) {
+    InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
+            @NonNull EditorInfo attribute, int controlFlags) {
         // If no method is currently selected, do nothing.
         if (mCurMethodId == null) {
             return mNoBinding;
@@ -1340,7 +1342,7 @@
             // If the client is changing, we need to switch over to the new
             // one.
             unbindCurrentClientLocked(InputMethodClient.UNBIND_REASON_SWITCH_CLIENT);
-            if (DEBUG) Slog.v(TAG, "switching to client: client = "
+            if (DEBUG) Slog.v(TAG, "switching to client: client="
                     + cs.client.asBinder() + " keyguard=" + mCurClientInKeyguard);
 
             // If the screen is on, inform the new client it is active
@@ -1442,15 +1444,26 @@
     }
 
     @Override
-    public InputBindResult startInput(IInputMethodClient client,
-            IInputContext inputContext, EditorInfo attribute, int controlFlags) {
+    public InputBindResult startInput(
+            /* @InputMethodClient.StartInputReason */ final int startInputReason,
+            IInputMethodClient client, IInputContext inputContext, EditorInfo attribute,
+            int controlFlags) {
         if (!calledFromValidUser()) {
             return null;
         }
         synchronized (mMethodMap) {
+            if (DEBUG) {
+                Slog.v(TAG, "startInput: reason="
+                        + InputMethodClient.getStartInputReason(startInputReason)
+                        + " client = " + client.asBinder()
+                        + " inputContext=" + inputContext
+                        + " attribute=" + attribute
+                        + " controlFlags=#" + Integer.toHexString(controlFlags));
+            }
             final long ident = Binder.clearCallingIdentity();
             try {
-                return startInputLocked(client, inputContext, attribute, controlFlags);
+                return startInputLocked(startInputReason, client, inputContext, attribute,
+                        controlFlags);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -2151,17 +2164,21 @@
     }
 
     @Override
-    public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken,
-            int controlFlags, int softInputMode, int windowFlags,
-            EditorInfo attribute, IInputContext inputContext) {
+    public InputBindResult windowGainedFocus(
+            /* @InputMethodClient.StartInputReason */ final int startInputReason,
+            IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
+            int windowFlags, EditorInfo attribute, IInputContext inputContext) {
         // Needs to check the validity before clearing calling identity
         final boolean calledFromValidUser = calledFromValidUser();
-
         InputBindResult res = null;
         long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mMethodMap) {
-                if (DEBUG) Slog.v(TAG, "windowGainedFocus: " + client.asBinder()
+                if (DEBUG) Slog.v(TAG, "windowGainedFocus: reason="
+                        + InputMethodClient.getStartInputReason(startInputReason)
+                        + " client=" + client.asBinder()
+                        + " inputContext=" + inputContext
+                        + " attribute=" + attribute
                         + " controlFlags=#" + Integer.toHexString(controlFlags)
                         + " softInputMode=#" + Integer.toHexString(softInputMode)
                         + " windowFlags=#" + Integer.toHexString(windowFlags));
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index cd29050..da2c8c5c 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -859,13 +859,11 @@
             if (DEBUG_SCREENSHOTS) Slog.d(TAG_SCREENSHOTS, "\tTaking screenshot");
 
             // When this flag is set, we currently take the fullscreen screenshot of the activity
-            // but scaled inversely by the density.  This gives us a "good-enough" fullscreen
-            // thumbnail to use within SystemUI without using enormous amounts of memory on high
-            // density devices.
+            // but scaled to half the size.  This gives us a "good-enough" fullscreen thumbnail to
+            // use within SystemUI while keeping memory usage low.
             if (mService.mTakeFullscreenScreenshots) {
-                Context context = mService.mContext;
                 w = h = -1;
-                scale = (1f / Math.max(1f, context.getResources().getDisplayMetrics().density));
+                scale = 0.5f;
             }
             return mWindowManager.screenshotApplications(who.appToken, Display.DEFAULT_DISPLAY,
                     w, h, scale);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index ff829ff..424b902 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -24,7 +24,6 @@
 import android.app.ActivityManagerNative;
 import android.app.IActivityManager;
 import android.app.IStopUserCallback;
-import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -216,14 +215,15 @@
     private final SparseArray<Bundle> mAppliedUserRestrictions = new SparseArray<>();
 
     /**
-     * User restrictions set by {@link DevicePolicyManager} that should be applied to all users,
-     * including guests.
+     * User restrictions set by {@link com.android.server.devicepolicy.DevicePolicyManagerService}
+     * that should be applied to all users, including guests.
      */
     @GuardedBy("mRestrictionsLock")
     private Bundle mDevicePolicyGlobalUserRestrictions;
 
     /**
-     * User restrictions set by {@link DevicePolicyManager} for each user.
+     * User restrictions set by {@link com.android.server.devicepolicy.DevicePolicyManagerService}
+     * for each user.
      */
     @GuardedBy("mRestrictionsLock")
     private final SparseArray<Bundle> mDevicePolicyLocalUserRestrictions = new SparseArray<>();
@@ -248,6 +248,12 @@
 
     private final LocalService mLocalService;
 
+    @GuardedBy("mUsersLock")
+    private boolean mIsDeviceManaged;
+
+    @GuardedBy("mUsersLock")
+    private final SparseBooleanArray mIsUserManaged = new SparseBooleanArray();
+
     @GuardedBy("mUserRestrictionsListeners")
     private final ArrayList<UserRestrictionsListener> mUserRestrictionsListeners =
             new ArrayList<>();
@@ -509,11 +515,9 @@
             if (!userInfo.isAdmin()) {
                 return false;
             }
+            // restricted profile can be created if there is no DO set and the admin user has no PO;
+            return !mIsDeviceManaged && !mIsUserManaged.get(userId);
         }
-        DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(
-                Context.DEVICE_POLICY_SERVICE);
-        // restricted profile can be created if there is no DO set and the admin user has no PO
-        return !dpm.isDeviceManaged() && dpm.getProfileOwnerAsUser(userId) == null;
     }
 
     /*
@@ -1596,11 +1600,10 @@
                 if (UserManager.isSplitSystemUser()
                         && !isGuest && !isManagedProfile && getPrimaryUser() == null) {
                     flags |= UserInfo.FLAG_PRIMARY;
-                    DevicePolicyManager devicePolicyManager = (DevicePolicyManager)
-                            mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
-                    if (devicePolicyManager == null
-                            || !devicePolicyManager.isDeviceManaged()) {
-                        flags |= UserInfo.FLAG_ADMIN;
+                    synchronized (mUsersLock) {
+                        if (!mIsDeviceManaged) {
+                            flags |= UserInfo.FLAG_ADMIN;
+                        }
                     }
                 }
                 userId = getNextAvailableId();
@@ -1865,6 +1868,13 @@
         // Remove this user from the list
         synchronized (mUsersLock) {
             mUsers.remove(userHandle);
+            mIsUserManaged.delete(userHandle);
+        }
+        synchronized (mRestrictionsLock) {
+            mBaseUserRestrictions.remove(userHandle);
+            mAppliedUserRestrictions.remove(userHandle);
+            mCachedEffectiveUserRestrictions.remove(userHandle);
+            mDevicePolicyLocalUserRestrictions.remove(userHandle);
         }
         // Remove user file
         AtomicFile userFile = new AtomicFile(new File(mUsersDir, userHandle + XML_SUFFIX));
@@ -2376,9 +2386,10 @@
                     if (user == null) {
                         continue;
                     }
+                    final int userId = user.id;
                     pw.print("  "); pw.print(user);
                     pw.print(" serialNo="); pw.print(user.serialNumber);
-                    if (mRemovingUserIds.get(mUsers.keyAt(i))) {
+                    if (mRemovingUserIds.get(userId)) {
                         pw.print(" <removing> ");
                     }
                     if (user.partial) {
@@ -2403,6 +2414,8 @@
                         sb.append(" ago");
                         pw.println(sb);
                     }
+                    pw.print("    Has profile owner: ");
+                    pw.println(mIsUserManaged.get(userId));
                     pw.println("    Restrictions:");
                     synchronized (mRestrictionsLock) {
                         UserRestrictionsUtils.dumpRestrictions(
@@ -2427,6 +2440,10 @@
             synchronized (mGuestRestrictions) {
                 UserRestrictionsUtils.dumpRestrictions(pw, "    ", mGuestRestrictions);
             }
+            synchronized (mUsersLock) {
+                pw.println();
+                pw.println("  Device managed: " + mIsDeviceManaged);
+            }
         }
     }
 
@@ -2507,6 +2524,20 @@
                 mUserRestrictionsListeners.remove(listener);
             }
         }
+
+        @Override
+        public void setDeviceManaged(boolean isManaged) {
+            synchronized (mUsersLock) {
+                mIsDeviceManaged = isManaged;
+            }
+        }
+
+        @Override
+        public void setUserManaged(int userId, boolean isManaged) {
+            synchronized (mUsersLock) {
+                mIsUserManaged.put(userId, isManaged);
+            }
+        }
     }
 
     private class Shell extends ShellCommand {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f80a611..8199250 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1071,7 +1071,7 @@
         }
 
         Owners newOwners() {
-            return new Owners(mContext);
+            return new Owners(mContext, getUserManager(), getUserManagerInternal());
         }
 
         UserManager getUserManager() {
@@ -2150,20 +2150,24 @@
     }
 
     private void ensureDeviceOwnerUserStarted() {
-        if (mOwners.hasDeviceOwner()) {
-            final int userId = mOwners.getDeviceOwnerUserId();
-            if (VERBOSE_LOG) {
-                Log.v(LOG_TAG, "Starting non-system DO user: " + userId);
+        final int userId;
+        synchronized (this) {
+            if (!mOwners.hasDeviceOwner()) {
+                return;
             }
-            if (userId != UserHandle.USER_SYSTEM) {
-                try {
-                    mInjector.getIActivityManager().startUserInBackground(userId);
+            userId = mOwners.getDeviceOwnerUserId();
+        }
+        if (VERBOSE_LOG) {
+            Log.v(LOG_TAG, "Starting non-system DO user: " + userId);
+        }
+        if (userId != UserHandle.USER_SYSTEM) {
+            try {
+                mInjector.getIActivityManager().startUserInBackground(userId);
 
-                    // STOPSHIP Prevent the DO user from being killed.
+                // STOPSHIP Prevent the DO user from being killed.
 
-                } catch (RemoteException e) {
-                    Slog.w(LOG_TAG, "Exception starting user", e);
-                }
+            } catch (RemoteException e) {
+                Slog.w(LOG_TAG, "Exception starting user", e);
             }
         }
     }
@@ -4584,7 +4588,7 @@
                     + " for device owner");
         }
         synchronized (this) {
-            enforceCanSetDeviceOwner(userId);
+            enforceCanSetDeviceOwnerLocked(userId);
 
             // Shutting down backup manager service permanently.
             long ident = mInjector.binderClearCallingIdentity();
@@ -4751,7 +4755,7 @@
                     + " not installed for userId:" + userHandle);
         }
         synchronized (this) {
-            enforceCanSetProfileOwner(userHandle);
+            enforceCanSetProfileOwnerLocked(userHandle);
             mOwners.setProfileOwner(who, ownerName, userHandle);
             mOwners.writeProfileOwner(userHandle);
             return true;
@@ -4953,7 +4957,7 @@
      * - SYSTEM_UID
      * - adb if there are not accounts.
      */
-    private void enforceCanSetProfileOwner(int userHandle) {
+    private void enforceCanSetProfileOwnerLocked(int userHandle) {
         UserInfo info = mUserManager.getUserInfo(userHandle);
         if (info == null) {
             // User doesn't exist.
@@ -4995,7 +4999,7 @@
      * The device owner can only be set before the setup phase of the primary user has completed,
      * except for adb if no accounts or additional users are present on the device.
      */
-    private void enforceCanSetDeviceOwner(int userId) {
+    private void enforceCanSetDeviceOwnerLocked(int userId) {
         if (mOwners.hasDeviceOwner()) {
             throw new IllegalStateException("Trying to set the device owner, but device owner "
                     + "is already set.");
@@ -6819,20 +6823,22 @@
     public boolean isProvisioningAllowed(String action) {
         final int callingUserId = mInjector.userHandleGetCallingUserId();
         if (DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE.equals(action)) {
-            if (mOwners.hasDeviceOwner()) {
-                if (!mInjector.userManagerIsSplitSystemUser()) {
-                    // Only split-system-user systems support managed-profiles in combination with
-                    // device-owner.
-                    return false;
-                }
-                if (mOwners.getDeviceOwnerUserId() != UserHandle.USER_SYSTEM) {
-                    // Only system device-owner supports managed-profiles. Non-system device-owner
-                    // doesn't.
-                    return false;
-                }
-                if (callingUserId == UserHandle.USER_SYSTEM) {
-                    // Managed-profiles cannot be setup on the system user, only regular users.
-                    return false;
+            synchronized (this) {
+                if (mOwners.hasDeviceOwner()) {
+                    if (!mInjector.userManagerIsSplitSystemUser()) {
+                        // Only split-system-user systems support managed-profiles in combination with
+                        // device-owner.
+                        return false;
+                    }
+                    if (mOwners.getDeviceOwnerUserId() != UserHandle.USER_SYSTEM) {
+                        // Only system device-owner supports managed-profiles. Non-system device-owner
+                        // doesn't.
+                        return false;
+                    }
+                    if (callingUserId == UserHandle.USER_SYSTEM) {
+                        // Managed-profiles cannot be setup on the system user, only regular users.
+                        return false;
+                    }
                 }
             }
             if (getProfileOwner(callingUserId) != null) {
@@ -6877,8 +6883,10 @@
     }
 
     private boolean isDeviceOwnerProvisioningAllowed(int callingUserId) {
-        if (mOwners.hasDeviceOwner()) {
-            return false;
+        synchronized (this) {
+            if (mOwners.hasDeviceOwner()) {
+                return false;
+            }
         }
         if (getProfileOwner(callingUserId) != null) {
             return false;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index 435de7a..f7de0b3 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -23,6 +23,7 @@
 import android.os.Environment;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.os.UserManagerInternal;
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -50,6 +51,9 @@
 /**
  * Stores and restores state for the Device and Profile owners. By definition there can be
  * only one device owner, but there may be a profile owner for each user.
+ *
+ * <p>This class is not thread safe.  (i.e. access to this class must always be synchronized
+ * in the caller side.)
  */
 class Owners {
     private static final String TAG = "DevicePolicyManagerService";
@@ -78,8 +82,8 @@
 
     private static final String TAG_SYSTEM_UPDATE_POLICY = "system-update-policy";
 
-    private final Context mContext;
     private final UserManager mUserManager;
+    private final UserManagerInternal mUserManagerInternal;
 
     // Internal state for the device owner package.
     private OwnerInfo mDeviceOwner;
@@ -92,44 +96,48 @@
     // Local system update policy controllable by device owner.
     private SystemUpdatePolicy mSystemUpdatePolicy;
 
-    public Owners(Context context) {
-        mContext = context;
-        mUserManager = context.getSystemService(UserManager.class);
+    public Owners(Context context, UserManager userManager,
+            UserManagerInternal userManagerInternal) {
+        mUserManager = userManager;
+        mUserManagerInternal = userManagerInternal;
     }
 
     /**
      * Load configuration from the disk.
      */
     void load() {
-        synchronized (this) {
-            // First, try to read from the legacy file.
-            final File legacy = getLegacyConfigFileWithTestOverride();
+        // First, try to read from the legacy file.
+        final File legacy = getLegacyConfigFileWithTestOverride();
 
-            if (readLegacyOwnerFile(legacy)) {
-                if (DEBUG) {
-                    Log.d(TAG, "Legacy config file found.");
-                }
+        final List<UserInfo> users = mUserManager.getUsers();
 
-                // Legacy file exists, write to new files and remove the legacy one.
-                writeDeviceOwner();
-                for (int userId : getProfileOwnerKeys()) {
-                    writeProfileOwner(userId);
-                }
-                if (DEBUG) {
-                    Log.d(TAG, "Deleting legacy config file");
-                }
-                if (!legacy.delete()) {
-                    Slog.e(TAG, "Failed to remove the legacy setting file");
-                }
-            } else {
-                // No legacy file, read from the new format files.
-                new DeviceOwnerReadWriter().readFromFileLocked();
-
-                final List<UserInfo> users = mUserManager.getUsers();
-                for (UserInfo ui : users) {
-                    new ProfileOwnerReadWriter(ui.id).readFromFileLocked();
-                }
+        if (readLegacyOwnerFile(legacy)) {
+            if (DEBUG) {
+                Log.d(TAG, "Legacy config file found.");
             }
+
+            // Legacy file exists, write to new files and remove the legacy one.
+            writeDeviceOwner();
+            for (int userId : getProfileOwnerKeys()) {
+                writeProfileOwner(userId);
+            }
+            if (DEBUG) {
+                Log.d(TAG, "Deleting legacy config file");
+            }
+            if (!legacy.delete()) {
+                Slog.e(TAG, "Failed to remove the legacy setting file");
+            }
+        } else {
+            // No legacy file, read from the new format files.
+            new DeviceOwnerReadWriter().readFromFileLocked();
+
+            for (UserInfo ui : users) {
+                new ProfileOwnerReadWriter(ui.id).readFromFileLocked();
+            }
+        }
+        mUserManagerInternal.setDeviceManaged(hasDeviceOwner());
+        for (UserInfo ui : users) {
+            mUserManagerInternal.setUserManaged(ui.id, hasProfileOwner(ui.id));
         }
         if (hasDeviceOwner() && hasProfileOwner(getDeviceOwnerUserId())) {
             Slog.w(TAG, String.format("User %d has both DO and PO, which is not supported",
@@ -169,21 +177,27 @@
             boolean userRestrictionsMigrated) {
         mDeviceOwner = new OwnerInfo(ownerName, admin, userRestrictionsMigrated);
         mDeviceOwnerUserId = userId;
+
+        mUserManagerInternal.setDeviceManaged(true);
     }
 
     void clearDeviceOwner() {
         mDeviceOwner = null;
         mDeviceOwnerUserId = UserHandle.USER_NULL;
+
+        mUserManagerInternal.setDeviceManaged(false);
     }
 
     void setProfileOwner(ComponentName admin, String ownerName, int userId) {
         // For a newly set PO, there's no need for migration.
         mProfileOwners.put(userId, new OwnerInfo(ownerName, admin,
                 /* userRestrictionsMigrated =*/ true));
+        mUserManagerInternal.setUserManaged(userId, true);
     }
 
     void removeProfileOwner(int userId) {
         mProfileOwners.remove(userId);
+        mUserManagerInternal.setUserManaged(userId, false);
     }
 
     ComponentName getProfileOwnerComponent(int userId) {
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 56d6fc0..435b602 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -52,7 +52,7 @@
         private final File mProfileOwnerBase;
 
         public OwnersTestable(DpmMockContext context) {
-            super(context);
+            super(context, context.userManager, context.userManagerInternal);
             mLegacyFile = new File(context.dataDir, LEGACY_FILE);
             mDeviceOwnerFile = new File(context.dataDir, DEVICE_OWNER_FILE);
             mProfileOwnerBase = new File(context.dataDir, PROFILE_OWNER_FILE_BASE);
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
index 8899e53..01c3c50 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
@@ -184,8 +184,10 @@
     }
 
     @Override
-    public InputBindResult startInput(IInputMethodClient client, IInputContext inputContext,
-            EditorInfo attribute, int controlFlags) throws RemoteException {
+    public InputBindResult startInput(
+            /* @InputMethodClient.StartInputReason */ int startInputReason,
+            IInputMethodClient client, IInputContext inputContext, EditorInfo attribute,
+            int controlFlags) throws RemoteException {
         // TODO Auto-generated method stub
         return null;
     }
@@ -226,9 +228,11 @@
     }
 
     @Override
-    public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken,
-            int controlFlags, int softInputMode, int windowFlags, EditorInfo attribute,
-            IInputContext inputContext) throws RemoteException {
+    public InputBindResult windowGainedFocus(
+            /* @InputMethodClient.StartInputReason */ int startInputReason,
+            IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
+            int windowFlags, EditorInfo attribute, IInputContext inputContext)
+            throws RemoteException {
         // TODO Auto-generated method stub
         return null;
     }