Merge "Correct GradientDrawable outline alpha computation"
diff --git a/api/current.txt b/api/current.txt
index feb43da..ca176b6 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -43255,7 +43255,7 @@
   }
 
   public final class SpellCheckerSubtype implements android.os.Parcelable {
-    ctor public SpellCheckerSubtype(int, java.lang.String, java.lang.String);
+    ctor public deprecated SpellCheckerSubtype(int, java.lang.String, java.lang.String);
     method public boolean containsExtraValueKey(java.lang.String);
     method public int describeContents();
     method public java.lang.CharSequence getDisplayName(android.content.Context, java.lang.String, android.content.pm.ApplicationInfo);
diff --git a/api/system-current.txt b/api/system-current.txt
index bc7983a..dcb0852 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -45594,7 +45594,7 @@
   }
 
   public final class SpellCheckerSubtype implements android.os.Parcelable {
-    ctor public SpellCheckerSubtype(int, java.lang.String, java.lang.String);
+    ctor public deprecated SpellCheckerSubtype(int, java.lang.String, java.lang.String);
     method public boolean containsExtraValueKey(java.lang.String);
     method public int describeContents();
     method public java.lang.CharSequence getDisplayName(android.content.Context, java.lang.String, android.content.pm.ApplicationInfo);
diff --git a/api/test-current.txt b/api/test-current.txt
index 3bcfd67..da02754 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -43257,7 +43257,7 @@
   }
 
   public final class SpellCheckerSubtype implements android.os.Parcelable {
-    ctor public SpellCheckerSubtype(int, java.lang.String, java.lang.String);
+    ctor public deprecated SpellCheckerSubtype(int, java.lang.String, java.lang.String);
     method public boolean containsExtraValueKey(java.lang.String);
     method public int describeContents();
     method public java.lang.CharSequence getDisplayName(android.content.Context, java.lang.String, android.content.pm.ApplicationInfo);
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 11df9a3..488063b 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -5631,7 +5631,7 @@
         final int matchAxisFilter = ((lp.width == MATCH_PARENT ? FLAG_LAYOUT_AXIS_HORIZONTAL : 0)
                 | (lp.height == MATCH_PARENT ? FLAG_LAYOUT_AXIS_VERTICAL : 0)) & axisFilter;
 
-        if (matchAxisFilter != 0) {
+        if (matchAxisFilter != 0 || wrapAxisFilter != 0) {
             final ViewParent parent = getParent();
             if (parent != null) {
                 // If our parent depends on us for an axis, then our layout can also be affected
diff --git a/core/java/android/view/textservice/SpellCheckerInfo.java b/core/java/android/view/textservice/SpellCheckerInfo.java
index 2167862..491de78 100644
--- a/core/java/android/view/textservice/SpellCheckerInfo.java
+++ b/core/java/android/view/textservice/SpellCheckerInfo.java
@@ -117,7 +117,9 @@
                             a.getString(com.android.internal.R.styleable
                                     .SpellChecker_Subtype_subtypeLocale),
                             a.getString(com.android.internal.R.styleable
-                                    .SpellChecker_Subtype_subtypeExtraValue));
+                                    .SpellChecker_Subtype_subtypeExtraValue),
+                            a.getInt(com.android.internal.R.styleable
+                                    .SpellChecker_Subtype_subtypeId, 0));
                     mSubtypes.add(subtype);
                 }
             }
diff --git a/core/java/android/view/textservice/SpellCheckerSubtype.java b/core/java/android/view/textservice/SpellCheckerSubtype.java
index 77fd002..f2b03cc 100644
--- a/core/java/android/view/textservice/SpellCheckerSubtype.java
+++ b/core/java/android/view/textservice/SpellCheckerSubtype.java
@@ -36,12 +36,21 @@
 /**
  * This class is used to specify meta information of a subtype contained in a spell checker.
  * Subtype can describe locale (e.g. en_US, fr_FR...) used for settings.
+ *
+ * @see SpellCheckerInfo
+ *
+ * @attr ref android.R.styleable#SpellChecker_Subtype_label
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeLocale
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeExtraValue
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeId
  */
 public final class SpellCheckerSubtype implements Parcelable {
     private static final String TAG = SpellCheckerSubtype.class.getSimpleName();
     private static final String EXTRA_VALUE_PAIR_SEPARATOR = ",";
     private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "=";
+    private static final int SUBTYPE_ID_NONE = 0;
 
+    private final int mSubtypeId;
     private final int mSubtypeHashCode;
     private final int mSubtypeNameResId;
     private final String mSubtypeLocale;
@@ -49,16 +58,40 @@
     private HashMap<String, String> mExtraValueHashMapCache;
 
     /**
-     * Constructor
+     * Constructor.
+     *
+     * <p>There is no public API that requires developers to instantiate custom
+     * {@link SpellCheckerSubtype} object.  Hence so far there is no need to make this constructor
+     * available in public API.</p>
+     *
      * @param nameId The name of the subtype
      * @param locale The locale supported by the subtype
      * @param extraValue The extra value of the subtype
+     * @param subtypeId The subtype ID that is supposed to be stable during package update.
+     *
+     * @hide
      */
-    public SpellCheckerSubtype(int nameId, String locale, String extraValue) {
+    public SpellCheckerSubtype(int nameId, String locale, String extraValue, int subtypeId) {
         mSubtypeNameResId = nameId;
         mSubtypeLocale = locale != null ? locale : "";
         mSubtypeExtraValue = extraValue != null ? extraValue : "";
-        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+        mSubtypeId = subtypeId;
+        mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
+                mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+    }
+
+    /**
+     * Constructor.
+     * @param nameId The name of the subtype
+     * @param locale The locale supported by the subtype
+     * @param extraValue The extra value of the subtype
+     *
+     * @deprecated There is no public API that requires developers to directly instantiate custom
+     * {@link SpellCheckerSubtype} objects right now.  Hence only the system is expected to be able
+     * to instantiate {@link SpellCheckerSubtype} object.
+     */
+    public SpellCheckerSubtype(int nameId, String locale, String extraValue) {
+        this(nameId, locale, extraValue, SUBTYPE_ID_NONE);
     }
 
     SpellCheckerSubtype(Parcel source) {
@@ -68,7 +101,9 @@
         mSubtypeLocale = s != null ? s : "";
         s = source.readString();
         mSubtypeExtraValue = s != null ? s : "";
-        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+        mSubtypeId = source.readInt();
+        mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
+                mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
     }
 
     /**
@@ -141,10 +176,13 @@
     public boolean equals(Object o) {
         if (o instanceof SpellCheckerSubtype) {
             SpellCheckerSubtype subtype = (SpellCheckerSubtype) o;
+            if (subtype.mSubtypeId != SUBTYPE_ID_NONE || mSubtypeId != SUBTYPE_ID_NONE) {
+                return (subtype.hashCode() == hashCode());
+            }
             return (subtype.hashCode() == hashCode())
-                && (subtype.getNameResId() == getNameResId())
-                && (subtype.getLocale().equals(getLocale()))
-                && (subtype.getExtraValue().equals(getExtraValue()));
+                    && (subtype.getNameResId() == getNameResId())
+                    && (subtype.getLocale().equals(getLocale()))
+                    && (subtype.getExtraValue().equals(getExtraValue()));
         }
         return false;
     }
@@ -197,6 +235,7 @@
         dest.writeInt(mSubtypeNameResId);
         dest.writeString(mSubtypeLocale);
         dest.writeString(mSubtypeExtraValue);
+        dest.writeInt(mSubtypeId);
     }
 
     public static final Parcelable.Creator<SpellCheckerSubtype> CREATOR
diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java
index ba868a1..bdb1e83 100644
--- a/core/java/android/widget/LinearLayout.java
+++ b/core/java/android/widget/LinearLayout.java
@@ -683,7 +683,7 @@
             }
         }
 
-        if (matchAxisFilter != 0) {
+        if (matchAxisFilter != 0 || wrapAxisFilter != 0) {
             final ViewParent parent = getParent();
             if (parent != null) {
                 // If our parent depends on us for an axis, then our layout can also be affected
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 9107b1f..531ba2f 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -1158,6 +1158,17 @@
                 lp.height = insets.getSystemWindowInsetBottom();
                 mNavigationGuard.setLayoutParams(lp);
             }
+            updateNavigationGuardColor();
+        }
+    }
+
+    void updateNavigationGuardColor() {
+        if (mNavigationGuard != null) {
+            // Make navigation bar guard invisible if the transparent color is specified.
+            // Only TRANSPARENT is sufficient for hiding the navigation bar if the no software
+            // keyboard is shown by IMS.
+            mNavigationGuard.setVisibility(mWindow.getNavigationBarColor() == Color.TRANSPARENT ?
+                    View.INVISIBLE : View.VISIBLE);
         }
     }
 
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 57d2244..2178344 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -3726,6 +3726,7 @@
         mForcedNavigationBarColor = true;
         if (mDecor != null) {
             mDecor.updateColorViews(null, false /* animate */);
+            mDecor.updateNavigationGuardColor();
         }
     }
 
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 34a66d0..9bca3d6 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -3095,6 +3095,13 @@
         <!-- The extra value of the subtype. This string can be any string and will be passed to
              the SpellChecker.  -->
         <attr name="subtypeExtraValue" format="string" />
+        <!-- The unique id for the subtype. The text service (spell checker) framework keeps track
+             of enabled subtypes by ID. When the spell checker package gets upgraded, enabled IDs
+             will stay enabled even if other attributes are different. If the ID is unspecified or
+             or explicitly specified to 0 in XML resources,
+             {@code Arrays.hashCode(new Object[] {subtypeLocale, extraValue}) will be used instead.
+              -->
+        <attr name="subtypeId" />
     </declare-styleable>
 
     <!-- Use <code>accessibility-service</code> as the root tag of the XML resource that
diff --git a/core/res/res/values/styles_holo.xml b/core/res/res/values/styles_holo.xml
index 841afd8..3cd60df 100644
--- a/core/res/res/values/styles_holo.xml
+++ b/core/res/res/values/styles_holo.xml
@@ -1176,7 +1176,7 @@
 
     <style name="Widget.Holo.Light.FastScroll" parent="Widget.Holo.FastScroll" />
 
-    <style name="Widget.Holo.SuggestionItem" parent="@android:attr/textAppearanceMedium">
+    <style name="Widget.Holo.SuggestionItem" parent="TextAppearance.Holo.Medium">
         <item name="background">@color/white</item>
         <item name="drawablePadding">8dip</item>
         <item name="ellipsize">marquee</item>
diff --git a/core/tests/coretests/src/android/view/textservice/SpellCheckerSubtypeTest.java b/core/tests/coretests/src/android/view/textservice/SpellCheckerSubtypeTest.java
index 157c815..73fdb10 100644
--- a/core/tests/coretests/src/android/view/textservice/SpellCheckerSubtypeTest.java
+++ b/core/tests/coretests/src/android/view/textservice/SpellCheckerSubtypeTest.java
@@ -29,12 +29,15 @@
  * TODO: Most of part can be, and probably should be, moved to CTS.
  */
 public class SpellCheckerSubtypeTest extends InstrumentationTestCase {
+    private static final int SUBTYPE_SUBTYPE_ID_NONE = 0;
     private static final String SUBTYPE_SUBTYPE_LOCALE_STRING_A = "en_GB";
     private static final int SUBTYPE_NAME_RES_ID_A = 0x12345;
     private static final String SUBTYPE_EXTRA_VALUE_A = "Key1=Value1,Key2=Value2";
+    private static final int SUBTYPE_SUBTYPE_ID_A = 42;
     private static final String SUBTYPE_SUBTYPE_LOCALE_STRING_B = "en_IN";
     private static final int SUBTYPE_NAME_RES_ID_B = 0x54321;
     private static final String SUBTYPE_EXTRA_VALUE_B = "Key3=Value3,Key4=Value4";
+    private static final int SUBTYPE_SUBTYPE_ID_B = -42;
 
     private static int defaultHashCodeAlgorithm(String locale, String extraValue) {
         return Arrays.hashCode(new Object[] {locale, extraValue});
@@ -55,10 +58,9 @@
     }
 
     @SmallTest
-    public void testSubtype() throws Exception {
+    public void testSubtypeWithNoSubtypeId() throws Exception {
         final SpellCheckerSubtype subtype = new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A,
-                SUBTYPE_SUBTYPE_LOCALE_STRING_A, SUBTYPE_EXTRA_VALUE_A);
-
+                SUBTYPE_SUBTYPE_LOCALE_STRING_A, SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE);
         assertEquals(SUBTYPE_NAME_RES_ID_A, subtype.getNameResId());
         assertEquals(SUBTYPE_SUBTYPE_LOCALE_STRING_A, subtype.getLocale());
         assertEquals("Value1", subtype.getExtraValueOf("Key1"));
@@ -80,6 +82,26 @@
                 clonedSubtype.hashCode());
     }
 
+    public void testSubtypeWithSubtypeId() throws Exception {
+        final SpellCheckerSubtype subtype = new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A,
+                SUBTYPE_SUBTYPE_LOCALE_STRING_A, SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A);
+
+        assertEquals(SUBTYPE_NAME_RES_ID_A, subtype.getNameResId());
+        assertEquals(SUBTYPE_SUBTYPE_LOCALE_STRING_A, subtype.getLocale());
+        assertEquals("Value1", subtype.getExtraValueOf("Key1"));
+        assertEquals("Value2", subtype.getExtraValueOf("Key2"));
+        // Similar to "SubtypeId" in InputMethodSubtype, "SubtypeId" in SpellCheckerSubtype enables
+        // developers to specify a stable and consistent ID for each subtype.
+        assertEquals(SUBTYPE_SUBTYPE_ID_A, subtype.hashCode());
+
+        final SpellCheckerSubtype clonedSubtype = cloneViaParcel(subtype);
+        assertEquals(SUBTYPE_NAME_RES_ID_A, clonedSubtype.getNameResId());
+        assertEquals(SUBTYPE_SUBTYPE_LOCALE_STRING_A, clonedSubtype.getLocale());
+        assertEquals("Value1", clonedSubtype.getExtraValueOf("Key1"));
+        assertEquals("Value2", clonedSubtype.getExtraValueOf("Key2"));
+        assertEquals(SUBTYPE_SUBTYPE_ID_A, clonedSubtype.hashCode());
+    }
+
     @SmallTest
     public void testGetLocaleObject() throws Exception {
         assertEquals(new Locale("en"), new SpellCheckerSubtype(
@@ -130,5 +152,54 @@
                         SUBTYPE_EXTRA_VALUE_A),
                 new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
                         SUBTYPE_EXTRA_VALUE_B));
+
+        // If subtype ID is 0 (== SUBTYPE_SUBTYPE_ID_NONE), we keep the same behavior.
+        assertEquals(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE));
+        assertNotEqual(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_B, SUBTYPE_SUBTYPE_LOCALE_STRING_B,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE));
+        assertNotEqual(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_B,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE));
+        assertNotEqual(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_NONE),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_B, SUBTYPE_SUBTYPE_ID_NONE));
+
+        // If subtype ID is not 0, we test the equality based only on the subtype ID.
+        assertEquals(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A));
+        assertEquals(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_B, SUBTYPE_SUBTYPE_LOCALE_STRING_B,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A));
+        assertEquals(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_B,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A));
+        assertEquals(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_B, SUBTYPE_SUBTYPE_ID_A));
+        assertNotEqual(
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_A),
+                new SpellCheckerSubtype(SUBTYPE_NAME_RES_ID_A, SUBTYPE_SUBTYPE_LOCALE_STRING_A,
+                        SUBTYPE_EXTRA_VALUE_A, SUBTYPE_SUBTYPE_ID_B));
     }
 }
diff --git a/libs/hwui/AssetAtlas.cpp b/libs/hwui/AssetAtlas.cpp
index 7e09699..41411a9 100644
--- a/libs/hwui/AssetAtlas.cpp
+++ b/libs/hwui/AssetAtlas.cpp
@@ -79,13 +79,13 @@
 // Entries
 ///////////////////////////////////////////////////////////////////////////////
 
-AssetAtlas::Entry* AssetAtlas::getEntry(const SkBitmap* bitmap) const {
-    ssize_t index = mEntries.indexOfKey(bitmap->pixelRef());
+AssetAtlas::Entry* AssetAtlas::getEntry(const SkPixelRef* pixelRef) const {
+    ssize_t index = mEntries.indexOfKey(pixelRef);
     return index >= 0 ? mEntries.valueAt(index) : nullptr;
 }
 
-Texture* AssetAtlas::getEntryTexture(const SkBitmap* bitmap) const {
-    ssize_t index = mEntries.indexOfKey(bitmap->pixelRef());
+Texture* AssetAtlas::getEntryTexture(const SkPixelRef* pixelRef) const {
+    ssize_t index = mEntries.indexOfKey(pixelRef);
     return index >= 0 ? mEntries.valueAt(index)->texture : nullptr;
 }
 
diff --git a/libs/hwui/AssetAtlas.h b/libs/hwui/AssetAtlas.h
index f1cd0b4..a037725 100644
--- a/libs/hwui/AssetAtlas.h
+++ b/libs/hwui/AssetAtlas.h
@@ -148,15 +148,15 @@
 
     /**
      * Returns the entry in the atlas associated with the specified
-     * bitmap. If the bitmap is not in the atlas, return NULL.
+     * pixelRef. If the pixelRef is not in the atlas, return NULL.
      */
-    Entry* getEntry(const SkBitmap* bitmap) const;
+    Entry* getEntry(const SkPixelRef* pixelRef) const;
 
     /**
      * Returns the texture for the atlas entry associated with the
-     * specified bitmap. If the bitmap is not in the atlas, return NULL.
+     * specified pixelRef. If the pixelRef is not in the atlas, return NULL.
      */
-    Texture* getEntryTexture(const SkBitmap* bitmap) const;
+    Texture* getEntryTexture(const SkPixelRef* pixelRef) const;
 
 private:
     void createEntries(Caches& caches, int64_t* map, int count);
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
index b56b1e4..fde12dd 100644
--- a/libs/hwui/BakedOpDispatcher.cpp
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -31,20 +31,182 @@
 namespace android {
 namespace uirenderer {
 
+static void storeTexturedRect(TextureVertex* vertices, const Rect& bounds, const Rect& texCoord) {
+    vertices[0] = { bounds.left, bounds.top, texCoord.left, texCoord.top };
+    vertices[1] = { bounds.right, bounds.top, texCoord.right, texCoord.top };
+    vertices[2] = { bounds.left, bounds.bottom, texCoord.left, texCoord.bottom };
+    vertices[3] = { bounds.right, bounds.bottom, texCoord.right, texCoord.bottom };
+}
+
+void BakedOpDispatcher::onMergedBitmapOps(BakedOpRenderer& renderer,
+        const MergedBakedOpList& opList) {
+
+    const BakedOpState& firstState = *(opList.states[0]);
+    const SkBitmap* bitmap = (static_cast<const BitmapOp*>(opList.states[0]->op))->bitmap;
+
+    AssetAtlas::Entry* entry = renderer.renderState().assetAtlas().getEntry(bitmap->pixelRef());
+    Texture* texture = entry ? entry->texture : renderer.caches().textureCache.get(bitmap);
+    if (!texture) return;
+    const AutoTexture autoCleanup(texture);
+
+    TextureVertex vertices[opList.count * 4];
+    Rect texCoords(0, 0, 1, 1);
+    if (entry) {
+        entry->uvMapper.map(texCoords);
+    }
+    // init to non-empty, so we can safely expandtoCoverRect
+    Rect totalBounds = firstState.computedState.clippedBounds;
+    for (size_t i = 0; i < opList.count; i++) {
+        const BakedOpState& state = *(opList.states[i]);
+        TextureVertex* rectVerts = &vertices[i * 4];
+        Rect opBounds = state.computedState.clippedBounds;
+        if (CC_LIKELY(state.computedState.transform.isPureTranslate())) {
+            // pure translate, so snap (same behavior as onBitmapOp)
+            opBounds.snapToPixelBoundaries();
+        }
+        storeTexturedRect(rectVerts, opBounds, texCoords);
+        renderer.dirtyRenderTarget(opBounds);
+
+        totalBounds.expandToCover(opBounds);
+    }
+
+    const int textureFillFlags = (bitmap->colorType() == kAlpha_8_SkColorType)
+            ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(firstState.roundRectClipState)
+            .setMeshTexturedIndexedQuads(vertices, opList.count * 6)
+            .setFillTexturePaint(*texture, textureFillFlags, firstState.op->paint, firstState.alpha)
+            .setTransform(Matrix4::identity(), TransformFlags::None)
+            .setModelViewOffsetRect(0, 0, totalBounds) // don't snap here, we snap per-quad above
+            .build();
+    renderer.renderGlop(nullptr, opList.clipSideFlags ? &opList.clip : nullptr, 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);
+}
+
+enum class TextRenderType {
+    Defer,
+    Flush
+};
+
+static void renderTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state,
+        const Rect* renderClip, TextRenderType renderType) {
+    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);
+    }
+    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, renderClip,
+            x, y, pureTranslate, alpha, mode, op.paint);
+
+    bool forceFinish = (renderType == TextRenderType::Flush);
+    bool mustDirtyRenderTarget = renderer.offscreenRenderTarget();
+    const Rect* localOpClip = pureTranslate ? &state.computedState.clipRect : nullptr;
+    fontRenderer.renderPosText(op.paint, localOpClip,
+            (const char*) op.glyphs, op.glyphCount, x, y,
+            op.positions, mustDirtyRenderTarget ? &layerBounds : nullptr, &functor, forceFinish);
+
+    if (mustDirtyRenderTarget) {
+        if (!pureTranslate) {
+            transform.mapRect(layerBounds);
+        }
+        renderer.dirtyRenderTarget(layerBounds);
+    }
+}
+
+void BakedOpDispatcher::onMergedTextOps(BakedOpRenderer& renderer,
+        const MergedBakedOpList& opList) {
+    const Rect* clip = opList.clipSideFlags ? &opList.clip : nullptr;
+    for (size_t i = 0; i < opList.count; i++) {
+        const BakedOpState& state = *(opList.states[i]);
+        const TextOp& op = *(static_cast<const TextOp*>(state.op));
+        TextRenderType renderType = (i + 1 == opList.count)
+                ? TextRenderType::Flush : TextRenderType::Defer;
+        renderTextOp(renderer, op, state, clip, renderType);
+    }
+}
+
 void BakedOpDispatcher::onRenderNodeOp(BakedOpRenderer&, const RenderNodeOp&, const BakedOpState&) {
     LOG_ALWAYS_FATAL("unsupported operation");
 }
 
-void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer& renderer, const BeginLayerOp& op, const BakedOpState& state) {
+void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer&, const BeginLayerOp&, const BakedOpState&) {
     LOG_ALWAYS_FATAL("unsupported operation");
 }
 
-void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer& renderer, const EndLayerOp& op, const BakedOpState& state) {
+void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer&, const EndLayerOp&, const BakedOpState&) {
     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);
@@ -153,89 +315,9 @@
     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
+    const Rect* clip = state.computedState.clipSideFlags ? &state.computedState.clipRect : nullptr;
+    renderTextOp(renderer, op, state, clip, TextRenderType::Flush);
 }
 
 void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) {
diff --git a/libs/hwui/BakedOpDispatcher.h b/libs/hwui/BakedOpDispatcher.h
index caf14bf..0e763d9 100644
--- a/libs/hwui/BakedOpDispatcher.h
+++ b/libs/hwui/BakedOpDispatcher.h
@@ -26,16 +26,21 @@
 /**
  * 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 "onMergedBitmapOps(...)" style methods for mergeable op types
+#define X(Type) \
+        static void onMerged##Type##s(BakedOpRenderer& renderer, const MergedBakedOpList& opList);
+    MAP_MERGED_OPS(X)
+#undef X
+
     // Declares all "onBitmapOp(...)" style methods for every op type
-#define DISPATCH_METHOD(Type) \
+#define X(Type) \
         static void on##Type(BakedOpRenderer& renderer, const Type& op, const BakedOpState& state);
-    MAP_OPS(DISPATCH_METHOD);
+    MAP_OPS(X)
+#undef X
+
 };
 
 }; // namespace uirenderer
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp
index 6cdc320..93a9406 100644
--- a/libs/hwui/BakedOpRenderer.cpp
+++ b/libs/hwui/BakedOpRenderer.cpp
@@ -121,30 +121,35 @@
 }
 
 Texture* BakedOpRenderer::getTexture(const SkBitmap* bitmap) {
-    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap);
+    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap->pixelRef());
     if (!texture) {
         return mCaches.textureCache.get(bitmap);
     }
     return texture;
 }
 
-void BakedOpRenderer::renderGlop(const BakedOpState& state, const Glop& glop) {
-    bool useScissor = state.computedState.clipSideFlags != OpClipSideFlags::None;
-    mRenderState.scissor().setEnabled(useScissor);
-    if (useScissor) {
-        const Rect& clip = state.computedState.clipRect;
-        mRenderState.scissor().set(clip.left, mRenderTarget.viewportHeight - clip.bottom,
-            clip.getWidth(), clip.getHeight());
+void BakedOpRenderer::renderGlop(const Rect* dirtyBounds, const Rect* clip, const Glop& glop) {
+    mRenderState.scissor().setEnabled(clip != nullptr);
+    if (clip) {
+        mRenderState.scissor().set(clip->left, mRenderTarget.viewportHeight - clip->bottom,
+            clip->getWidth(), clip->getHeight());
     }
-    if (mRenderTarget.offscreenBuffer) { // TODO: not with multi-draw
+    if (dirtyBounds && mRenderTarget.offscreenBuffer) {
         // register layer damage to draw-back region
-        const Rect& uiDirty = state.computedState.clippedBounds;
-        android::Rect dirty(uiDirty.left, uiDirty.top, uiDirty.right, uiDirty.bottom);
+        android::Rect dirty(dirtyBounds->left, dirtyBounds->top,
+                dirtyBounds->right, dirtyBounds->bottom);
         mRenderTarget.offscreenBuffer->region.orSelf(dirty);
     }
     mRenderState.render(glop, mRenderTarget.orthoMatrix);
     if (!mRenderTarget.frameBufferId) mHasDrawn = true;
 }
 
+void BakedOpRenderer::dirtyRenderTarget(const Rect& uiDirty) {
+    if (mRenderTarget.offscreenBuffer) {
+        android::Rect dirty(uiDirty.left, uiDirty.top, uiDirty.right, uiDirty.bottom);
+        mRenderTarget.offscreenBuffer->region.orSelf(dirty);
+    }
+}
+
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h
index 62d1838..d7600db 100644
--- a/libs/hwui/BakedOpRenderer.h
+++ b/libs/hwui/BakedOpRenderer.h
@@ -67,7 +67,16 @@
     Texture* getTexture(const SkBitmap* bitmap);
     const LightInfo& getLightInfo() { return mLightInfo; }
 
-    void renderGlop(const BakedOpState& state, const Glop& glop);
+    void renderGlop(const BakedOpState& state, const Glop& glop) {
+        bool useScissor = state.computedState.clipSideFlags != OpClipSideFlags::None;
+        renderGlop(&state.computedState.clippedBounds,
+                useScissor ? &state.computedState.clipRect : nullptr,
+                glop);
+    }
+
+    void renderGlop(const Rect* dirtyBounds, const Rect* clip, const Glop& glop);
+    bool offscreenRenderTarget() { return mRenderTarget.offscreenBuffer != nullptr; }
+    void dirtyRenderTarget(const Rect& dirtyRect);
     bool didDraw() { return mHasDrawn; }
 private:
     void setViewport(uint32_t width, uint32_t height);
diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h
index 9a40c3b..983c27b 100644
--- a/libs/hwui/BakedOpState.h
+++ b/libs/hwui/BakedOpState.h
@@ -38,6 +38,16 @@
 }
 
 /**
+ * Holds a list of BakedOpStates of ops that can be drawn together
+ */
+struct MergedBakedOpList {
+    const BakedOpState*const* states;
+    size_t count;
+    int clipSideFlags;
+    Rect clip;
+};
+
+/**
  * Holds the resolved clip, transform, and bounds of a recordedOp, when replayed with a snapshot
  */
 class ResolvedRenderState {
diff --git a/libs/hwui/ClipArea.cpp b/libs/hwui/ClipArea.cpp
index a9d1e42..fd6f0b5 100644
--- a/libs/hwui/ClipArea.cpp
+++ b/libs/hwui/ClipArea.cpp
@@ -26,7 +26,7 @@
 static void handlePoint(Rect& transformedBounds, const Matrix4& transform, float x, float y) {
     Vertex v = {x, y};
     transform.mapPoint(v.x, v.y);
-    transformedBounds.expandToCoverVertex(v.x, v.y);
+    transformedBounds.expandToCover(v.x, v.y);
 }
 
 Rect transformAndCalculateBounds(const Rect& r, const Matrix4& transform) {
diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h
index e7cc464..92217edc 100644
--- a/libs/hwui/DisplayListOp.h
+++ b/libs/hwui/DisplayListOp.h
@@ -612,7 +612,7 @@
     AssetAtlas::Entry* getAtlasEntry(OpenGLRenderer& renderer) {
         if (!mEntryValid) {
             mEntryValid = true;
-            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap);
+            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap->pixelRef());
         }
         return mEntry;
     }
@@ -777,7 +777,7 @@
     AssetAtlas::Entry* getAtlasEntry(OpenGLRenderer& renderer) {
         if (!mEntryValid) {
             mEntryValid = true;
-            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap);
+            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap->pixelRef());
         }
         return mEntry;
     }
diff --git a/libs/hwui/FontRenderer.cpp b/libs/hwui/FontRenderer.cpp
index 47654f4..9c8649f 100644
--- a/libs/hwui/FontRenderer.cpp
+++ b/libs/hwui/FontRenderer.cpp
@@ -75,7 +75,8 @@
             .setTransform(bakedState->computedState.transform, transformFlags)
             .setModelViewOffsetRect(0, 0, Rect(0, 0, 0, 0))
             .build();
-    renderer->renderGlop(*bakedState, glop);
+    // Note: don't pass dirty bounds here, so user must manage passing dirty bounds to renderer
+    renderer->renderGlop(nullptr, clip, glop);
 #else
     GlopBuilder(renderer->mRenderState, renderer->mCaches, &glop)
             .setRoundRectClipState(renderer->currentSnapshot()->roundRectClipState)
diff --git a/libs/hwui/FontRenderer.h b/libs/hwui/FontRenderer.h
index 87cfe7f..ff4dc4a 100644
--- a/libs/hwui/FontRenderer.h
+++ b/libs/hwui/FontRenderer.h
@@ -57,6 +57,7 @@
 #if HWUI_NEW_OPS
             BakedOpRenderer* renderer,
             const BakedOpState* bakedState,
+            const Rect* clip,
 #else
             OpenGLRenderer* renderer,
 #endif
@@ -65,6 +66,7 @@
         : renderer(renderer)
 #if HWUI_NEW_OPS
         , bakedState(bakedState)
+        , clip(clip)
 #endif
         , x(x)
         , y(y)
@@ -79,6 +81,7 @@
 #if HWUI_NEW_OPS
     BakedOpRenderer* renderer;
     const BakedOpState* bakedState;
+    const Rect* clip;
 #else
     OpenGLRenderer* renderer;
 #endif
diff --git a/libs/hwui/GlopBuilder.h b/libs/hwui/GlopBuilder.h
index 6270dcb..b647b90 100644
--- a/libs/hwui/GlopBuilder.h
+++ b/libs/hwui/GlopBuilder.h
@@ -53,7 +53,7 @@
     GlopBuilder& setMeshTexturedUvQuad(const UvMapper* uvMapper, const Rect uvs);
     GlopBuilder& setMeshVertexBuffer(const VertexBuffer& vertexBuffer, bool shadowInterp);
     GlopBuilder& setMeshIndexedQuads(Vertex* vertexData, int quadCount);
-    GlopBuilder& setMeshTexturedMesh(TextureVertex* vertexData, int elementCount); // TODO: use indexed quads
+    GlopBuilder& setMeshTexturedMesh(TextureVertex* vertexData, int elementCount); // TODO: delete
     GlopBuilder& setMeshColoredTexturedMesh(ColorTextureVertex* vertexData, int elementCount); // TODO: use indexed quads
     GlopBuilder& setMeshTexturedIndexedQuads(TextureVertex* vertexData, int elementCount); // TODO: take quadCount
     GlopBuilder& setMeshPatchQuads(const Patch& patch);
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
index 9cbd9c2d..9460361 100644
--- a/libs/hwui/OpReorderer.cpp
+++ b/libs/hwui/OpReorderer.cpp
@@ -202,6 +202,9 @@
         if (newClipSideFlags & OpClipSideFlags::Bottom) mClipRect.bottom = opClip.bottom;
     }
 
+    bool getClipSideFlags() const { return mClipSideFlags; }
+    const Rect& getClipRect() const { return mClipRect; }
+
 private:
     int mClipSideFlags = 0;
     Rect mClipRect;
@@ -291,12 +294,31 @@
     }
 }
 
-void OpReorderer::LayerReorderer::replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers) const {
+void OpReorderer::LayerReorderer::replayBakedOpsImpl(void* arg,
+        BakedOpReceiver* unmergedReceivers, MergedOpReceiver* mergedReceivers) const {
     ATRACE_NAME("flush drawing commands");
     for (const BatchBase* batch : mBatches) {
-        // TODO: different behavior based on batch->isMerging()
-        for (const BakedOpState* op : batch->getOps()) {
-            receivers[op->op->opId](arg, *op->op, *op);
+        size_t size = batch->getOps().size();
+        if (size > 1 && batch->isMerging()) {
+            int opId = batch->getOps()[0]->op->opId;
+            const MergingOpBatch* mergingBatch = static_cast<const MergingOpBatch*>(batch);
+            MergedBakedOpList data = {
+                    batch->getOps().data(),
+                    size,
+                    mergingBatch->getClipSideFlags(),
+                    mergingBatch->getClipRect()
+            };
+            if (data.clipSideFlags) {
+                // if right or bottom sides aren't used to clip, init them to viewport bounds
+                // in the clip rect, so it can be used to scissor
+                if (!(data.clipSideFlags & OpClipSideFlags::Right)) data.clip.right = width;
+                if (!(data.clipSideFlags & OpClipSideFlags::Bottom)) data.clip.bottom = height;
+            }
+            mergedReceivers[opId](arg, data);
+        } else {
+            for (const BakedOpState* op : batch->getOps()) {
+                unmergedReceivers[op->op->opId](arg, *op);
+            }
         }
     }
 }
@@ -639,7 +661,8 @@
 #define OP_RECEIVER(Type) \
         [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); },
 void OpReorderer::deferNodeOps(const RenderNode& renderNode) {
-    static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = {
+    typedef void (*OpDispatcher) (OpReorderer& reorderer, const RecordedOp& op);
+    static OpDispatcher receivers[] = {
         MAP_OPS(OP_RECEIVER)
     };
 
@@ -692,42 +715,57 @@
 }
 
 void OpReorderer::onBitmapOp(const BitmapOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
 
-    mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
-    // TODO: AssetAtlas
-    currentLayer().deferMergeableOp(mAllocator, bakedStateOp, OpBatchType::Bitmap, mergeId);
+    // Don't merge non-simply transformed or neg scale ops, SET_TEXTURE doesn't handle rotation
+    // Don't merge A8 bitmaps - the paint's color isn't compared by mergeId, or in
+    // MergingDrawBatch::canMergeWith()
+    if (bakedState->computedState.transform.isSimple()
+            && bakedState->computedState.transform.positiveScale()
+            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode
+            && op.bitmap->colorType() != kAlpha_8_SkColorType) {
+        mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
+        // TODO: AssetAtlas in mergeId
+        currentLayer().deferMergeableOp(mAllocator, bakedState, OpBatchType::Bitmap, mergeId);
+    } else {
+        currentLayer().deferUnmergeableOp(mAllocator, bakedState, OpBatchType::Bitmap);
+    }
 }
 
 void OpReorderer::onLinesOp(const LinesOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, OpBatchType::Vertices);
-
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
 }
 
 void OpReorderer::onRectOp(const RectOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, tessellatedBatchId(*op.paint));
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
 }
 
 void OpReorderer::onSimpleRectsOp(const SimpleRectsOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, OpBatchType::Vertices);
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, OpBatchType::Vertices);
 }
 
 void OpReorderer::onTextOp(const TextOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
 
     // TODO: better handling of shader (since we won't care about color then)
     batchid_t batchId = op.paint->getColor() == SK_ColorBLACK
             ? OpBatchType::Text : OpBatchType::ColorText;
-    mergeid_t mergeId = reinterpret_cast<mergeid_t>(op.paint->getColor());
-    currentLayer().deferMergeableOp(mAllocator, bakedStateOp, batchId, mergeId);
+
+    if (bakedState->computedState.transform.isPureTranslate()
+            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode) {
+        mergeid_t mergeId = reinterpret_cast<mergeid_t>(op.paint->getColor());
+        currentLayer().deferMergeableOp(mAllocator, bakedState, batchId, mergeId);
+    } else {
+        currentLayer().deferUnmergeableOp(mAllocator, bakedState, batchId);
+    }
 }
 
 void OpReorderer::saveForLayer(uint32_t layerWidth, uint32_t layerHeight,
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
index 00df8b0..fc77c61 100644
--- a/libs/hwui/OpReorderer.h
+++ b/libs/hwui/OpReorderer.h
@@ -58,7 +58,8 @@
 }
 
 class OpReorderer : public CanvasStateClient {
-    typedef std::function<void(void*, const RecordedOp&, const BakedOpState&)> BakedOpDispatcher;
+    typedef void (*BakedOpReceiver)(void*, const BakedOpState&);
+    typedef void (*MergedOpReceiver)(void*, const MergedBakedOpList& opList);
 
     /**
      * Stores the deferred render operations and state used to compute ordering
@@ -87,7 +88,7 @@
         void deferMergeableOp(LinearAllocator& allocator,
                 BakedOpState* op, batchid_t batchId, mergeid_t mergeId);
 
-        void replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers) const;
+        void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers, MergedOpReceiver*) const;
 
         bool empty() const {
             return mBatches.empty();
@@ -132,19 +133,44 @@
      * It constructs a lookup array of lambdas, which allows a recorded BakeOpState to use
      * state->op->opId to lookup a receiver that will be called when the op is replayed.
      *
-     * For example a BitmapOp would resolve, via the lambda lookup, to calling:
-     *
-     * StaticDispatcher::onBitmapOp(Renderer& renderer, const BitmapOp& op, const BakedOpState& state);
      */
-#define BAKED_OP_RECEIVER(Type) \
-    [](void* internalRenderer, const RecordedOp& op, const BakedOpState& state) { \
-        StaticDispatcher::on##Type(*(static_cast<Renderer*>(internalRenderer)), static_cast<const Type&>(op), state); \
-    },
     template <typename StaticDispatcher, typename Renderer>
     void replayBakedOps(Renderer& renderer) {
-        static BakedOpDispatcher receivers[] = {
-            MAP_OPS(BAKED_OP_RECEIVER)
+        /**
+         * defines a LUT of lambdas which allow a recorded BakedOpState to use state->op->opId to
+         * dispatch the op via a method on a static dispatcher when the op is replayed.
+         *
+         * For example a BitmapOp would resolve, via the lambda lookup, to calling:
+         *
+         * StaticDispatcher::onBitmapOp(Renderer& renderer, const BitmapOp& op, const BakedOpState& state);
+         */
+        #define X(Type) \
+                [](void* renderer, const BakedOpState& state) { \
+                    StaticDispatcher::on##Type(*(static_cast<Renderer*>(renderer)), static_cast<const Type&>(*(state.op)), state); \
+                },
+        static BakedOpReceiver unmergedReceivers[] = {
+            MAP_OPS(X)
         };
+        #undef X
+
+        /**
+         * defines a LUT of lambdas which allow merged arrays of BakedOpState* to be passed to a
+         * static dispatcher when the group of merged ops is replayed. Unmergeable ops trigger
+         * a LOG_ALWAYS_FATAL().
+         */
+        #define X(Type) \
+                [](void* renderer, const MergedBakedOpList& opList) { \
+                    LOG_ALWAYS_FATAL("op type %d does not support merging", opList.states[0]->op->opId); \
+                },
+        #define Y(Type) \
+                [](void* renderer, const MergedBakedOpList& opList) { \
+                    StaticDispatcher::onMerged##Type##s(*(static_cast<Renderer*>(renderer)), opList); \
+                },
+        static MergedOpReceiver mergedReceivers[] = {
+            MAP_OPS_BASED_ON_MERGEABILITY(X, Y)
+        };
+        #undef X
+        #undef Y
 
         // Relay through layers in reverse order, since layers
         // later in the list will be drawn by earlier ones
@@ -153,18 +179,18 @@
             if (layer.renderNode) {
                 // cached HW layer - can't skip layer if empty
                 renderer.startRepaintLayer(layer.offscreenBuffer, layer.repaintRect);
-                layer.replayBakedOpsImpl((void*)&renderer, receivers);
+                layer.replayBakedOpsImpl((void*)&renderer, unmergedReceivers, mergedReceivers);
                 renderer.endLayer();
             } else if (!layer.empty()) { // save layer - skip entire layer if empty
                 layer.offscreenBuffer = renderer.startTemporaryLayer(layer.width, layer.height);
-                layer.replayBakedOpsImpl((void*)&renderer, receivers);
+                layer.replayBakedOpsImpl((void*)&renderer, unmergedReceivers, mergedReceivers);
                 renderer.endLayer();
             }
         }
 
         const LayerReorderer& fbo0 = mLayerReorderers[0];
         renderer.startFrame(fbo0.width, fbo0.height, fbo0.repaintRect);
-        fbo0.replayBakedOpsImpl((void*)&renderer, receivers);
+        fbo0.replayBakedOpsImpl((void*)&renderer, unmergedReceivers, mergedReceivers);
         renderer.endFrame();
     }
 
@@ -213,7 +239,7 @@
 
     void deferRenderNodeOp(const RenderNodeOp& op);
 
-    void replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers);
+    void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers);
 
     SkPath* createFrameAllocatedPath() {
         mFrameAllocatedPaths.emplace_back(new SkPath);
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index e386b1c..2cb32c4 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -1525,7 +1525,7 @@
         colors = tempColors.get();
     }
 
-    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap);
+    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap->pixelRef());
     const UvMapper& mapper(getMapper(texture));
 
     for (int32_t y = 0; y < meshHeight; y++) {
@@ -2146,7 +2146,7 @@
     bool status;
 #if HWUI_NEW_OPS
     LOG_ALWAYS_FATAL("unsupported");
-    TextDrawFunctor functor(nullptr, nullptr, x, y, pureTranslate, alpha, mode, paint);
+    TextDrawFunctor functor(nullptr, nullptr, nullptr, x, y, pureTranslate, alpha, mode, paint);
 #else
     TextDrawFunctor functor(this, x, y, pureTranslate, alpha, mode, paint);
 #endif
@@ -2190,7 +2190,7 @@
     SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(paint);
 #if HWUI_NEW_OPS
     LOG_ALWAYS_FATAL("unsupported");
-    TextDrawFunctor functor(nullptr, nullptr, 0.0f, 0.0f, false, alpha, mode, paint);
+    TextDrawFunctor functor(nullptr, nullptr, nullptr, 0.0f, 0.0f, false, alpha, mode, paint);
 #else
     TextDrawFunctor functor(this, 0.0f, 0.0f, false, alpha, mode, paint);
 #endif
@@ -2308,7 +2308,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 
 Texture* OpenGLRenderer::getTexture(const SkBitmap* bitmap) {
-    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap);
+    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap->pixelRef());
     if (!texture) {
         return mCaches.textureCache.get(bitmap);
     }
diff --git a/libs/hwui/PathTessellator.cpp b/libs/hwui/PathTessellator.cpp
index b57b8f0..9246237 100644
--- a/libs/hwui/PathTessellator.cpp
+++ b/libs/hwui/PathTessellator.cpp
@@ -799,7 +799,7 @@
     dstBuffer.alloc<TYPE>(numPoints * verticesPerPoint + (numPoints - 1) * 2);
 
     for (int i = 0; i < count; i += 2) {
-        bounds.expandToCoverVertex(points[i + 0], points[i + 1]);
+        bounds.expandToCover(points[i + 0], points[i + 1]);
         dstBuffer.copyInto<TYPE>(srcBuffer, points[i + 0], points[i + 1]);
     }
     dstBuffer.createDegenerateSeparators<TYPE>(verticesPerPoint);
@@ -878,8 +878,8 @@
         }
 
         // calculate bounds
-        bounds.expandToCoverVertex(tempVerticesData[0].x, tempVerticesData[0].y);
-        bounds.expandToCoverVertex(tempVerticesData[1].x, tempVerticesData[1].y);
+        bounds.expandToCover(tempVerticesData[0].x, tempVerticesData[0].y);
+        bounds.expandToCover(tempVerticesData[1].x, tempVerticesData[1].y);
     }
 
     // since multiple objects tessellated into buffer, separate them with degen tris
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index b4a201e..b966401 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -37,21 +37,34 @@
 struct Vertex;
 
 /**
- * The provided macro is executed for each op type in order, with the results separated by commas.
+ * On of the provided macros is executed for each op type in order. The first will be used for ops
+ * that cannot merge, and the second for those that can.
  *
  * This serves as the authoritative list of ops, used for generating ID enum, and ID based LUTs.
  */
+#define MAP_OPS_BASED_ON_MERGEABILITY(U_OP_FN, M_OP_FN) \
+        M_OP_FN(BitmapOp) \
+        U_OP_FN(LinesOp) \
+        U_OP_FN(RectOp) \
+        U_OP_FN(RenderNodeOp) \
+        U_OP_FN(ShadowOp) \
+        U_OP_FN(SimpleRectsOp) \
+        M_OP_FN(TextOp) \
+        U_OP_FN(BeginLayerOp) \
+        U_OP_FN(EndLayerOp) \
+        U_OP_FN(LayerOp)
+
+/**
+ * The provided macro is executed for each op type in order. This is used in cases where
+ * merge-ability of ops doesn't matter.
+ */
 #define MAP_OPS(OP_FN) \
-        OP_FN(BitmapOp) \
-        OP_FN(LinesOp) \
-        OP_FN(RectOp) \
-        OP_FN(RenderNodeOp) \
-        OP_FN(ShadowOp) \
-        OP_FN(SimpleRectsOp) \
-        OP_FN(TextOp) \
-        OP_FN(BeginLayerOp) \
-        OP_FN(EndLayerOp) \
-        OP_FN(LayerOp)
+        MAP_OPS_BASED_ON_MERGEABILITY(OP_FN, OP_FN)
+
+#define NULL_OP_FN(Type)
+
+#define MAP_MERGED_OPS(OP_FN) \
+        MAP_OPS_BASED_ON_MERGEABILITY(NULL_OP_FN, OP_FN)
 
 // Generate OpId enum
 #define IDENTITY_FN(Type) Type,
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 69c686e..e6020cd 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -248,10 +248,7 @@
 
     Rect unmappedBounds(points[0], points[1], points[0], points[1]);
     for (int i = 2; i < floatCount; i += 2) {
-        unmappedBounds.left = std::min(unmappedBounds.left, points[i]);
-        unmappedBounds.right = std::max(unmappedBounds.right, points[i]);
-        unmappedBounds.top = std::min(unmappedBounds.top, points[i + 1]);
-        unmappedBounds.bottom = std::max(unmappedBounds.bottom, points[i + 1]);
+        unmappedBounds.expandToCover(points[i], points[i + 1]);
     }
 
     // since anything AA stroke with less than 1.0 pixel width is drawn with an alpha-reduced
@@ -413,6 +410,7 @@
     glyphs = refBuffer<glyph_t>(glyphs, glyphCount);
     positions = refBuffer<float>(positions, glyphCount * 2);
 
+    // TODO: either must account for text shadow in bounds, or record separate ops for text shadows
     addOp(new (alloc()) TextOp(
             Rect(boundsLeft, boundsTop, boundsRight, boundsBottom),
             *(mState.currentSnapshot()->transform),
diff --git a/libs/hwui/Rect.h b/libs/hwui/Rect.h
index 472aad7..30c925c 100644
--- a/libs/hwui/Rect.h
+++ b/libs/hwui/Rect.h
@@ -253,7 +253,18 @@
         bottom = ceilf(bottom);
     }
 
-    void expandToCoverVertex(float x, float y) {
+    /*
+     * Similar to unionWith, except this makes the assumption that both rects are non-empty
+     * to avoid both emptiness checks.
+     */
+    void expandToCover(const Rect& other) {
+        left = std::min(left, other.left);
+        top = std::min(top, other.top);
+        right = std::max(right, other.right);
+        bottom = std::max(bottom, other.bottom);
+    }
+
+    void expandToCover(float x, float y) {
         left = std::min(left, x);
         top = std::min(top, y);
         right = std::max(right, x);
diff --git a/libs/hwui/TextureCache.cpp b/libs/hwui/TextureCache.cpp
index a6c72a3..21901cf 100644
--- a/libs/hwui/TextureCache.cpp
+++ b/libs/hwui/TextureCache.cpp
@@ -138,7 +138,7 @@
 // in the cache (and is thus added to the cache)
 Texture* TextureCache::getCachedTexture(const SkBitmap* bitmap, AtlasUsageType atlasUsageType) {
     if (CC_LIKELY(mAssetAtlas != nullptr) && atlasUsageType == AtlasUsageType::Use) {
-        AssetAtlas::Entry* entry = mAssetAtlas->getEntry(bitmap);
+        AssetAtlas::Entry* entry = mAssetAtlas->getEntry(bitmap->pixelRef());
         if (CC_UNLIKELY(entry)) {
             return entry->texture;
         }
diff --git a/libs/hwui/VertexBuffer.h b/libs/hwui/VertexBuffer.h
index c0373ac..bdb5b7b 100644
--- a/libs/hwui/VertexBuffer.h
+++ b/libs/hwui/VertexBuffer.h
@@ -118,7 +118,7 @@
         TYPE* end = current + vertexCount;
         mBounds.set(current->x, current->y, current->x, current->y);
         for (; current < end; current++) {
-            mBounds.expandToCoverVertex(current->x, current->y);
+            mBounds.expandToCover(current->x, current->y);
         }
     }
 
diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h
index 9c1c0b9..0af9939 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -103,10 +103,11 @@
         return snapshot;
     }
 
-    static SkBitmap createSkBitmap(int width, int height) {
+    static SkBitmap createSkBitmap(int width, int height,
+            SkColorType colorType = kN32_SkColorType) {
         SkBitmap bitmap;
         SkImageInfo info = SkImageInfo::Make(width, height,
-                kN32_SkColorType, kPremul_SkAlphaType);
+                colorType, kPremul_SkAlphaType);
         bitmap.setInfo(info);
         bitmap.allocPixels(info);
         return bitmap;
diff --git a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
index 27adb12..6c64a32 100644
--- a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
@@ -62,7 +62,9 @@
         int cardIndexOffset = scrollPx / (cardSpacing + cardHeight);
         int pxOffset = -(scrollPx % (cardSpacing + cardHeight));
 
-        TestCanvas canvas(cardWidth, cardHeight);
+        TestCanvas canvas(
+                listView->stagingProperties().getWidth(),
+                listView->stagingProperties().getHeight());
         for (size_t ci = 0; ci < cards.size(); ci++) {
             // update card position
             auto card = cards[(ci + cardIndexOffset) % cards.size()];
@@ -121,9 +123,11 @@
             static SkBitmap filledBox = createBoxBitmap(true);
             static SkBitmap strokedBox = createBoxBitmap(false);
 
-            props.mutableOutline().setRoundRect(0, 0, cardWidth, cardHeight, dp(6), 1);
-            props.mutableOutline().setShouldClip(true);
-            canvas.drawColor(Color::White, SkXfermode::kSrcOver_Mode);
+            // TODO: switch to using round rect clipping, once merging correctly handles that
+            SkPaint roundRectPaint;
+            roundRectPaint.setAntiAlias(true);
+            roundRectPaint.setColor(Color::White);
+            canvas.drawRoundRect(0, 0, cardWidth, cardHeight, dp(6), dp(6), roundRectPaint);
 
             SkPaint textPaint;
             textPaint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
diff --git a/libs/hwui/tests/microbench/OpReordererBench.cpp b/libs/hwui/tests/microbench/OpReordererBench.cpp
index 406bfcc..ac2b15c 100644
--- a/libs/hwui/tests/microbench/OpReordererBench.cpp
+++ b/libs/hwui/tests/microbench/OpReordererBench.cpp
@@ -25,7 +25,7 @@
 #include "RecordingCanvas.h"
 #include "tests/common/TestUtils.h"
 #include "Vector.h"
-#include "microbench/MicroBench.h"
+#include "tests/microbench/MicroBench.h"
 
 #include <vector>
 
diff --git a/libs/hwui/tests/unit/OpReordererTests.cpp b/libs/hwui/tests/unit/OpReordererTests.cpp
index 98a430a..068e832 100644
--- a/libs/hwui/tests/unit/OpReordererTests.cpp
+++ b/libs/hwui/tests/unit/OpReordererTests.cpp
@@ -65,12 +65,22 @@
     virtual void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) {}
     virtual void endFrame() {}
 
-    // define virtual defaults for direct
-#define BASE_OP_METHOD(Type) \
+    // define virtual defaults for single draw methods
+#define X(Type) \
     virtual void on##Type(const Type&, const BakedOpState&) { \
         ADD_FAILURE() << #Type " not expected in this test"; \
     }
-    MAP_OPS(BASE_OP_METHOD)
+    MAP_OPS(X)
+#undef X
+
+    // define virtual defaults for merged draw methods
+#define X(Type) \
+    virtual void onMerged##Type##s(const MergedBakedOpList& opList) { \
+        ADD_FAILURE() << "Merged " #Type "s not expected in this test"; \
+    }
+    MAP_MERGED_OPS(X)
+#undef X
+
     int getIndex() { return mIndex; }
 
 protected:
@@ -83,11 +93,21 @@
  */
 class TestDispatcher {
 public:
-#define DISPATCHER_METHOD(Type) \
+    // define single op methods, which redirect to TestRendererBase
+#define X(Type) \
     static void on##Type(TestRendererBase& renderer, const Type& op, const BakedOpState& state) { \
         renderer.on##Type(op, state); \
     }
-    MAP_OPS(DISPATCHER_METHOD);
+    MAP_OPS(X);
+#undef X
+
+    // define merged op methods, which redirect to TestRendererBase
+#define X(Type) \
+    static void onMerged##Type##s(TestRendererBase& renderer, const MergedBakedOpList& opList) { \
+        renderer.onMerged##Type##s(opList); \
+    }
+    MAP_MERGED_OPS(X);
+#undef X
 };
 
 class FailRenderer : public TestRendererBase {};
@@ -153,7 +173,8 @@
 
     auto node = TestUtils::createNode(0, 0, 200, 200,
             [](RenderProperties& props, RecordingCanvas& canvas) {
-        SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
+        SkBitmap bitmap = TestUtils::createSkBitmap(10, 10,
+                kAlpha_8_SkColorType); // Disable merging by using alpha 8 bitmap
 
         // 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.
@@ -171,7 +192,7 @@
     SimpleBatchingTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(2 * LOOPS, renderer.getIndex())
-            << "Expect number of ops = 2 * loop count"; // TODO: force no merging
+            << "Expect number of ops = 2 * loop count";
 }
 
 TEST(OpReorderer, textStrikethroughBatching) {
@@ -181,8 +202,10 @@
         void onRectOp(const RectOp& op, const BakedOpState& state) override {
             EXPECT_TRUE(mIndex++ >= LOOPS) << "Strikethrough rects should be above all text";
         }
-        void onTextOp(const TextOp& op, const BakedOpState& state) override {
-            EXPECT_TRUE(mIndex++ < LOOPS) << "Text should be beneath all strikethrough rects";
+        void onMergedTextOps(const MergedBakedOpList& opList) override {
+            EXPECT_EQ(0, mIndex);
+            mIndex += opList.count;
+            EXPECT_EQ(5u, opList.count);
         }
     };
     auto node = TestUtils::createNode(0, 0, 200, 2000,
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 3d00e02..23ad1a81b 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -54,10 +54,6 @@
 
     final boolean voiceInteraction;
 
-    // Whether the window has a saved surface from last pause, which can be
-    // used to start an entering animation earlier.
-    boolean mHasSavedSurface;
-
     // Whether we're performing an entering animation with a saved surface.
     boolean mAnimatingWithSavedSurface;
 
@@ -316,39 +312,38 @@
         // currently animating with save surfaces. (If the app didn't even finish
         // drawing when the user exits, but we have a saved surface from last time,
         // we still want to keep that surface.)
-        mHasSavedSurface = allDrawn || mAnimatingWithSavedSurface;
-        if (mHasSavedSurface) {
-            if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
-                    "Saving surface: " + this);
-            return true;
+        return allDrawn || mAnimatingWithSavedSurface;
+    }
+
+    boolean hasSavedSurface() {
+        for (int i = windows.size() -1; i >= 0; i--) {
+            final WindowState ws = windows.get(i);
+            if (ws.hasSavedSurface()) {
+                return true;
+            }
         }
         return false;
     }
 
     void restoreSavedSurfaces() {
-        if (!mHasSavedSurface) {
+        if (!hasSavedSurface()) {
             return;
         }
 
         if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                 "Restoring saved surfaces: " + this + ", allDrawn=" + allDrawn);
 
-        mHasSavedSurface = false;
         mAnimatingWithSavedSurface = true;
         for (int i = windows.size() - 1; i >= 0; i--) {
             WindowState ws = windows.get(i);
-            ws.mWinAnimator.mDrawState = WindowStateAnimator.READY_TO_SHOW;
+            ws.restoreSavedSurface();
         }
     }
 
     void destroySavedSurfaces() {
-        if (mHasSavedSurface) {
-            if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
-                    "Destroying saved surface: " + this);
-            for (int i = windows.size() - 1; i >= 0; i--) {
-                final WindowState win = windows.get(i);
-                win.mWinAnimator.destroySurfaceLocked();
-            }
+        for (int i = windows.size() - 1; i >= 0; i--) {
+            WindowState win = windows.get(i);
+            win.destroySavedSurface();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 0b851da..5237a13 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2816,9 +2816,7 @@
             if (mInputMethodWindow == win) {
                 mInputMethodWindow = null;
             }
-            if (!win.shouldSaveSurface()) {
-                winAnimator.destroySurfaceLocked();
-            }
+            win.destroyOrSaveSurface();
         }
         //TODO (multidisplay): Magnification is supported only for the default
         if (mAccessibilityController != null
@@ -8985,11 +8983,10 @@
                             Slog.w(TAG, "LEAKED SURFACE (app token hidden): "
                                     + ws + " surface=" + wsa.mSurfaceController
                                     + " token=" + ws.mAppToken
-                                    + " saved=" + ws.mAppToken.mHasSavedSurface);
+                                    + " saved=" + ws.mAppToken.hasSavedSurface());
                             if (SHOW_TRANSACTIONS) logSurface(ws, "LEAK DESTROY", null);
                             wsa.destroySurface();
                             ws.setHasSurface(false);
-                            ws.mAppToken.mHasSavedSurface = false;
                             leakedSurface = true;
                         }
                     }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 5e38492..c9ded3a 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -84,6 +84,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static com.android.server.wm.WindowManagerService.DEBUG_ADD_REMOVE;
+import static com.android.server.wm.WindowManagerService.DEBUG_ANIM;
+import static com.android.server.wm.WindowManagerService.DEBUG_APP_TRANSITIONS;
 import static com.android.server.wm.WindowManagerService.DEBUG_CONFIGURATION;
 import static com.android.server.wm.WindowManagerService.DEBUG_FOCUS_LIGHT;
 import static com.android.server.wm.WindowManagerService.DEBUG_LAYOUT;
@@ -402,6 +404,10 @@
     /** When true this window can be displayed on screens owther than mOwnerUid's */
     private boolean mShowToOwnerOnly;
 
+    // Whether the window has a saved surface from last pause, which can be
+    // used to start an entering animation earlier.
+    public boolean mSurfaceSaved = false;
+
     /**
      * Wake lock for drawing.
      * Even though it's slightly more expensive to do so, we will use a separate wake lock
@@ -1670,22 +1676,45 @@
         return mAppToken != null && mAppToken.mAnimatingWithSavedSurface;
     }
 
-    boolean shouldSaveSurface() {
+    // Returns true if the surface is saved.
+    boolean destroyOrSaveSurface() {
+        Task task = getTask();
         if (ActivityManager.isLowRamDeviceStatic()) {
             // Don't save surfaces on Svelte devices.
-            return false;
-        }
-
-        Task task = getTask();
-        if (task == null || task.inHomeStack()
+            mSurfaceSaved = false;
+        } else if (task == null || task.inHomeStack()
                 || task.getTopVisibleAppToken() != mAppToken) {
             // Don't save surfaces for home stack apps. These usually resume and draw
             // first frame very fast. Saving surfaces are mostly a waste of memory.
             // Don't save if the window is not the topmost window.
-            return false;
+            mSurfaceSaved = false;
+        } else if (mAttachedWindow != null) {
+            mSurfaceSaved = false;
+        } else {
+            mSurfaceSaved = mAppToken.shouldSaveSurface();
         }
+        if (mSurfaceSaved == false) {
+            mWinAnimator.destroySurfaceLocked();
+        }
+        return mSurfaceSaved;
+    }
 
-        return mAppToken.shouldSaveSurface();
+    public void destroySavedSurface() {
+        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
+                "Destroying saved surface: " + this);
+
+        if (mSurfaceSaved) {
+            mWinAnimator.destroySurfaceLocked();
+        }
+    }
+
+    public boolean hasSavedSurface() {
+        return mSurfaceSaved;
+    }
+
+    public void restoreSavedSurface() {
+        mSurfaceSaved = false;
+        mWinAnimator.mDrawState = WindowStateAnimator.READY_TO_SHOW;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 132b1b6..1790fb3 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -726,13 +726,14 @@
     void destroySurfaceLocked() {
         final AppWindowToken wtoken = mWin.mAppToken;
         if (wtoken != null) {
-            wtoken.mHasSavedSurface = false;
             wtoken.mAnimatingWithSavedSurface = false;
             if (mWin == wtoken.startingWindow) {
                 wtoken.startingDisplayed = false;
             }
         }
 
+        mWin.mSurfaceSaved = false;
+
         if (mSurfaceController != null) {
             int i = mWin.mChildWindows.size();
             // When destroying a surface we want to make sure child windows
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index 2149019..3ae3be5 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -394,9 +394,7 @@
                 if (mWallpaperControllerLocked.isWallpaperTarget(win)) {
                     wallpaperDestroyed = true;
                 }
-                if (!win.shouldSaveSurface()) {
-                    win.mWinAnimator.destroySurfaceLocked();
-                }
+                win.destroyOrSaveSurface();
             } while (i > 0);
             mService.mDestroySurface.clear();
         }
@@ -1252,7 +1250,7 @@
                         + wtoken.startingDisplayed + " startingMoved="
                         + wtoken.startingMoved);
 
-                if (wtoken.mHasSavedSurface || wtoken.mAnimatingWithSavedSurface) {
+                if (wtoken.hasSavedSurface() || wtoken.mAnimatingWithSavedSurface) {
                     continue;
                 }
                 if (!wtoken.allDrawn && !wtoken.startingDisplayed && !wtoken.startingMoved) {