Merge "Change audio include path from system/core to system/media/audio"
diff --git a/api/current.txt b/api/current.txt
index e3658d7..822324f 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -14730,6 +14730,16 @@
     field public static final int WRITE_NON_BLOCKING = 1; // 0x1
   }
 
+  public static class AudioTrack.Builder {
+    ctor public AudioTrack.Builder();
+    method public android.media.AudioTrack build() throws java.lang.UnsupportedOperationException;
+    method public android.media.AudioTrack.Builder setAudioAttributes(android.media.AudioAttributes) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setAudioFormat(android.media.AudioFormat) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setBufferSizeInBytes(int) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setSessionId(int) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setTransferMode(int) throws java.lang.IllegalArgumentException;
+  }
+
   public static abstract interface AudioTrack.OnPlaybackPositionUpdateListener {
     method public abstract void onMarkerReached(android.media.AudioTrack);
     method public abstract void onPeriodicNotification(android.media.AudioTrack);
@@ -15969,6 +15979,23 @@
     method public abstract void onScanCompleted(java.lang.String, android.net.Uri);
   }
 
+  public final class MediaSync {
+    ctor public MediaSync();
+    method public void configureAudioTrack(android.media.AudioTrack, int);
+    method public void configureSurface(android.view.Surface);
+    method public final android.view.Surface createInputSurface();
+    method public void queueAudio(java.nio.ByteBuffer, int, int, long);
+    method public final void release();
+    method public void setCallback(android.media.MediaSync.Callback, android.os.Handler);
+    method public void setPlaybackRate(float, int);
+    field public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 0; // 0x0
+  }
+
+  public static abstract class MediaSync.Callback {
+    ctor public MediaSync.Callback();
+    method public abstract void onReturnAudioBuffer(android.media.MediaSync, java.nio.ByteBuffer, int);
+  }
+
   public class MediaSyncEvent {
     method public static android.media.MediaSyncEvent createEvent(int) throws java.lang.IllegalArgumentException;
     method public int getAudioSessionId();
diff --git a/api/system-current.txt b/api/system-current.txt
index 43cc40f..288d15b 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -15938,6 +15938,16 @@
     field public static final int WRITE_NON_BLOCKING = 1; // 0x1
   }
 
+  public static class AudioTrack.Builder {
+    ctor public AudioTrack.Builder();
+    method public android.media.AudioTrack build() throws java.lang.UnsupportedOperationException;
+    method public android.media.AudioTrack.Builder setAudioAttributes(android.media.AudioAttributes) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setAudioFormat(android.media.AudioFormat) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setBufferSizeInBytes(int) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setSessionId(int) throws java.lang.IllegalArgumentException;
+    method public android.media.AudioTrack.Builder setTransferMode(int) throws java.lang.IllegalArgumentException;
+  }
+
   public static abstract interface AudioTrack.OnPlaybackPositionUpdateListener {
     method public abstract void onMarkerReached(android.media.AudioTrack);
     method public abstract void onPeriodicNotification(android.media.AudioTrack);
@@ -17180,6 +17190,23 @@
     method public abstract void onScanCompleted(java.lang.String, android.net.Uri);
   }
 
+  public final class MediaSync {
+    ctor public MediaSync();
+    method public void configureAudioTrack(android.media.AudioTrack, int);
+    method public void configureSurface(android.view.Surface);
+    method public final android.view.Surface createInputSurface();
+    method public void queueAudio(java.nio.ByteBuffer, int, int, long);
+    method public final void release();
+    method public void setCallback(android.media.MediaSync.Callback, android.os.Handler);
+    method public void setPlaybackRate(float, int);
+    field public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 0; // 0x0
+  }
+
+  public static abstract class MediaSync.Callback {
+    ctor public MediaSync.Callback();
+    method public abstract void onReturnAudioBuffer(android.media.MediaSync, java.nio.ByteBuffer, int);
+  }
+
   public class MediaSyncEvent {
     method public static android.media.MediaSyncEvent createEvent(int) throws java.lang.IllegalArgumentException;
     method public int getAudioSessionId();
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 8832895..bf9a1ad 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -848,6 +848,15 @@
             = "android.settings.ZEN_MODE_PRIORITY_SETTINGS";
 
     /**
+     * Activity Action: Show Zen Mode automation configuration settings.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_ZEN_MODE_AUTOMATION_SETTINGS
+            = "android.settings.ZEN_MODE_AUTOMATION_SETTINGS";
+
+    /**
      * Activity Action: Show the regulatory information screen for the device.
      * <p>
      * In some cases, a matching Activity may not exist, so ensure you safeguard
diff --git a/core/java/android/provider/VoicemailContract.java b/core/java/android/provider/VoicemailContract.java
index 0da4fd5..efbb3b8 100644
--- a/core/java/android/provider/VoicemailContract.java
+++ b/core/java/android/provider/VoicemailContract.java
@@ -258,7 +258,7 @@
         public static Uri insert(Context context, Voicemail voicemail) {
             ContentResolver contentResolver = context.getContentResolver();
             ContentValues contentValues = getContentValues(voicemail);
-            return contentResolver.insert(Voicemails.CONTENT_URI, contentValues);
+            return contentResolver.insert(buildSourceUri(context.getPackageName()), contentValues);
         }
 
         /**
@@ -273,7 +273,7 @@
             int count = voicemails.size();
             for (int i = 0; i < count; i++) {
                 ContentValues contentValues = getContentValues(voicemails.get(i));
-                contentResolver.insert(Voicemails.CONTENT_URI, contentValues);
+                contentResolver.insert(buildSourceUri(context.getPackageName()), contentValues);
             }
             return count;
         }
diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java
index 041796b..6d2f368 100644
--- a/core/java/android/widget/ImageView.java
+++ b/core/java/android/widget/ImageView.java
@@ -687,12 +687,19 @@
         return mDrawMatrix;
     }
 
+    /**
+     * Adds a transformation {@link Matrix} that is applied
+     * to the view's drawable when it is drawn.  Allows custom scaling,
+     * translation, and perspective distortion.
+     * 
+     * @param matrix the transformation parameters in matrix form
+     */
     public void setImageMatrix(Matrix matrix) {
-        // collaps null and identity to just null
+        // collapse null and identity to just null
         if (matrix != null && matrix.isIdentity()) {
             matrix = null;
         }
-        
+
         // don't invalidate unless we're actually changing our matrix
         if (matrix == null && !mMatrix.isIdentity() ||
                 matrix != null && !mMatrix.equals(matrix)) {
diff --git a/core/java/com/android/internal/logging/MetricsLogger.java b/core/java/com/android/internal/logging/MetricsLogger.java
index 50d252b..1038543 100644
--- a/core/java/com/android/internal/logging/MetricsLogger.java
+++ b/core/java/com/android/internal/logging/MetricsLogger.java
@@ -40,6 +40,7 @@
 
     public static final int MANAGE_PERMISSIONS = 142;
     public static final int NOTIFICATION_ZEN_MODE_PRIORITY = 143;
+    public static final int NOTIFICATION_ZEN_MODE_AUTOMATION = 144;
 
     public static final int MANAGE_DOMAIN_URLS = 143;
 
diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp
index a7a925f..5552245 100644
--- a/core/jni/android_media_AudioTrack.cpp
+++ b/core/jni/android_media_AudioTrack.cpp
@@ -17,6 +17,8 @@
 
 #define LOG_TAG "AudioTrack-JNI"
 
+#include "android_media_AudioTrack.h"
+
 #include <JNIHelp.h>
 #include <JniConstants.h>
 #include "core_jni_helpers.h"
@@ -181,6 +183,12 @@
     env->SetLongField(thiz, javaAudioTrackFields.nativeTrackInJavaObj, (jlong)at.get());
     return old;
 }
+
+// ----------------------------------------------------------------------------
+sp<AudioTrack> android_media_AudioTrack_getAudioTrack(JNIEnv* env, jobject audioTrackObj) {
+    return getAudioTrack(env, audioTrackObj);
+}
+
 // ----------------------------------------------------------------------------
 static jint
 android_media_AudioTrack_setup(JNIEnv *env, jobject thiz, jobject weak_this,
diff --git a/core/jni/android_media_AudioTrack.h b/core/jni/android_media_AudioTrack.h
new file mode 100644
index 0000000..ef2aa66
--- /dev/null
+++ b/core/jni/android_media_AudioTrack.h
@@ -0,0 +1,34 @@
+/*
+ * 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_MEDIA_AUDIOTRACK_H
+#define ANDROID_MEDIA_AUDIOTRACK_H
+
+#include "jni.h"
+
+#include <utils/StrongPointer.h>
+
+namespace android {
+
+class AudioTrack;
+
+}; // namespace android
+
+/* Gets the underlying AudioTrack from an AudioTrack Java object. */
+extern android::sp<android::AudioTrack> android_media_AudioTrack_getAudioTrack(
+        JNIEnv* env, jobject audioTrackObj);
+
+#endif // ANDROID_MEDIA_AUDIOTRACK_H
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index a13282c..eb37619 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -3505,15 +3505,29 @@
         <!-- Sets a drawable as the content of this ImageView. -->
         <attr name="src" format="reference|color" />
         <!-- Controls how the image should be resized or moved to match the size
-             of this ImageView. -->
+             of this ImageView.  See {@link android.widget.ImageView.ScaleType} -->
         <attr name="scaleType">
+            <!-- Scale using the image matrix when drawing. See  
+                 {@link android.widget.ImageView#setImageMatrix(Matrix)}. -->
             <enum name="matrix" value="0" />
+            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#FILL}. -->
             <enum name="fitXY" value="1" />
+            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#START}. -->
             <enum name="fitStart" value="2" />
+            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#CENTER}. -->
             <enum name="fitCenter" value="3" />
+            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#END}. -->
             <enum name="fitEnd" value="4" />
+            <!-- Center the image in the view, but perform no scaling. -->
             <enum name="center" value="5" />
+            <!-- Scale the image uniformly (maintain the image's aspect ratio) so both dimensions 
+                 (width and height) of the image will be equal to or larger than the corresponding
+                 dimension of the view (minus padding). The image is then centered in the view. -->
             <enum name="centerCrop" value="6" />
+            <!-- Scale the image uniformly (maintain the image's aspect ratio) so that both
+                 dimensions (width and height) of the image will be equal to or less than the
+                 corresponding dimension of the view (minus padding). The image is then centered in
+                 the view. -->
             <enum name="centerInside" value="7" />
         </attr>
         <!-- Set this to true if you want the ImageView to adjust its bounds
diff --git a/docs/html/tools/testing-support-library/index.jd b/docs/html/tools/testing-support-library/index.jd
index aeace8e..c8c9ef5 100644
--- a/docs/html/tools/testing-support-library/index.jd
+++ b/docs/html/tools/testing-support-library/index.jd
@@ -391,7 +391,9 @@
 
   <p>
     To learn more about using Espresso, see the
-    <a href="{@docRoot}reference/android/support/test/package-summary.html">API reference</a>.
+    <a href="{@docRoot}reference/android/support/test/package-summary.html">API reference</a> and
+    <a href="{@docRoot}training/testing/ui-testing/espresso-testing.html">
+      Testing UI for a Single App</a> training.
   </p>
 
   <h3 id="UIAutomator">
@@ -531,7 +533,9 @@
 
   <p>
     To learn more about using UI Automator, see the
-    <a href="{@docRoot}reference/android/support/test/package-summary.html">API reference</a>.
+    <a href="{@docRoot}reference/android/support/test/package-summary.html">API reference</a> and
+    <a href="{@docRoot}training/testing/ui-testing/uiautomator-testing.html">
+      Testing UI for Multiple Apps</a> training.
   </p>
 
   <h2 id="setup">
diff --git a/docs/html/training/auto/start/index.jd b/docs/html/training/auto/start/index.jd
index 54500ac..22e7521 100644
--- a/docs/html/training/auto/start/index.jd
+++ b/docs/html/training/auto/start/index.jd
@@ -55,14 +55,6 @@
 setting up your development environment and meeting the the minimum requirements
 to enable an app to communicate with Auto.</p>
 
-<p class="note"><strong>Important:</strong> If you are planning to develop
-apps for Auto, you are encouraged to begin enabling and testing your
-apps now. However, Auto-enabled apps cannot be published at this time.
-Join the
-<a href="http://g.co/AndroidAutoDev" class="external-link">Auto
-Developers Google+ community</a> for updates on when you will be able to submit
-your Auto-enabled apps.</p>
-
 <h2 id="dev-project">Set Up an Auto Project</h2>
 <p>This section describes how to create a new app or modify an existing app to
 communicate with Auto.</p>
diff --git a/docs/html/training/testing/ui-testing/espresso-testing.jd b/docs/html/training/testing/ui-testing/espresso-testing.jd
new file mode 100644
index 0000000..e5e37f7
--- /dev/null
+++ b/docs/html/training/testing/ui-testing/espresso-testing.jd
@@ -0,0 +1,579 @@
+page.title=Testing UI for a Single App
+page.tags=testing,espresso
+trainingnavtop=true
+
+@jd:body
+
+<!-- This is the training bar -->
+<div id="tb-wrapper">
+<div id="tb">
+  <h2>Dependencies and Prerequisites</h2>
+
+        <ul>
+          <li>Android 2.2 (API level 8) or higher
+          </li>
+
+          <li>
+            <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support
+            Library</a>
+          </li>
+        </ul>
+
+        <h2>
+          This lesson teaches you to
+        </h2>
+
+        <ol>
+          <li>
+            <a href="#setup">Set Up Espresso</a>
+          </li>
+
+          <li>
+            <a href="#build">Create an Espresso Test Class</a>
+          </li>
+
+          <li>
+            <a href="#run">Run Espresso Tests on a Device or Emulator</a>
+          </li>
+        </ol>
+
+        <h2>
+          You should also read
+        </h2>
+
+        <ul>
+           <li><a href="{@docRoot}reference/android/support/test/package-summary.html">
+              Espresso API Reference</a></li>
+        </ul>
+
+        <h2>
+          Try it out
+        </h2>
+
+        <ul>
+          <li>
+            <a href="https://github.com/googlesamples/android-testing"
+            class="external-link">Espresso Code Samples</a>
+          </li>
+        </ul>
+      </div>
+    </div>
+
+    <p>
+      UI tests that involve user interactions
+      within a single app help to ensure that users do not
+      encounter unexpected results or have a poor experience when interacting with your app.
+      You should get into the habit of creating user interface (UI) tests if you need to verify
+      that the UI of your app is functioning correctly.
+    </p>
+
+    <p>
+      The Espresso testing framework, provided by the
+      <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>,
+      provides APIs for writing UI tests to simulate user interactions within a
+      single target app. Espresso tests can run on devices running Android 2.2 (API level 8) and
+      higher. A key benefit of using Espresso is that it provides automatic synchronization of test
+      actions with the UI of the app you are testing. Espresso detects when the main thread is idle,
+      so it is able to run your test commands at the appropriate time, improving the reliability of
+      your tests. This capability also relieves you from having to adding any timing workarounds,
+      such as a sleep period, in your test code.
+    </p>
+
+    <p>
+      The Espresso testing framework is an instrumentation-based API and works
+      with the
+      <a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">{@code
+      AndroidJUnitRunner}</a> test runner.
+    </p>
+
+    <h2 id="setup">
+      Set Up Espresso
+    </h2>
+
+    <p>
+      Before you begin using Espresso, you must:
+    </p>
+
+    <ul>
+      <li>
+        <strong>Install the Android Testing Support Library</strong>. The Espresso API is
+        located under the {@code com.android.support.test.espresso} package. These classes allow
+        you to create tests that use the Espresso testing framework. To learn how to install the
+        library, see <a href="{@docRoot}tools/testing-support-library/index.html#setup">
+        Testing Support Library Setup</a>.
+      </li>
+
+      <li>
+        <strong>Set up your project structure.</strong> In your Gradle project, the source code for
+        the target app that you want to test is typically placed under the {@code app/src/main}
+        folder. The source code for instrumentation tests, including
+        your Espresso tests, must be placed under the <code>app/src/androidTest</code> folder. To
+        learn more about setting up your project directory, see
+        <a href="{@docRoot}tools/projects/index.html">Managing Projects</a>.
+      </li>
+
+      <li>
+        <strong>Specify your Android testing dependencies</strong>. In order for the
+        <a href="{@docRoot}tools/building/plugin-for-gradle.html">Android Plug-in for Gradle</a> to
+        correctly build and run your Espresso tests, you must specify the following libraries in
+        the {@code build.gradle} file of your Android app module:
+
+        <pre>
+dependencies {
+    androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
+    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0'
+}
+</pre>
+      </li>
+
+      <li>
+        <strong>Turn off animations on your test device.</strong> Leaving system animations turned
+        on in the test device might cause unexpected results or may lead your test to fail. Turn
+        off animations from <em>Settings</em> by opening <em>Developing Options</em> and
+        turning all the following options off:
+        <ul>
+          <li>
+            <em>Window animation scale</em>
+          </li>
+
+          <li>
+            <em>Transition animation scale</em>
+          </li>
+
+          <li>
+            <em>Animator duration scale</em>
+          </li>
+        </ul>
+      </li>
+    </ul>
+
+    <h2 id="build">
+      Create an Espresso Test Class
+    </h2>
+
+    <p>
+      To create an Espresso test, create a Java class or an
+      {@link android.test.ActivityInstrumentationTestCase2}
+      subclass that follows this programming model:
+    </p>
+
+    <ol>
+      <li>Find the UI component you want to test in an {@link android.app.Activity} (for example, a
+      sign-in button in the app) by calling the
+      <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onView(org.hamcrest.Matcher<android.view.View>)">
+        {@code onView()}</a> method, or the
+      <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher<java.lang.Object>)">
+      {@code onData()}</a> method for {@link android.widget.AdapterView} controls.
+      </li>
+
+      <li>Simulate a specific user interaction to perform on that UI component, by calling the
+      <a href="{@docRoot}reference/android/support/test/espresso/ViewInteraction.html#perform(android.support.test.espresso.ViewAction...)">{@code ViewInteraction.perform()}</a>
+      or
+      <a href="{@docRoot}reference/android/support/test/espresso/DataInteraction.html#perform(android.support.test.espresso.ViewAction...)">{@code DataInteraction.perform()}</a>
+      method and passing in the user action (for example, click on the sign-in button). To sequence
+      multiple actions on the same UI component, chain them using a comma-separated list in your
+      method argument.
+      </li>
+
+      <li>Repeat the steps above as necessary, to simulate a user flow across multiple
+      activities in the target app.
+      </li>
+
+      <li>Use the
+    <a href="{@docRoot}reference/android/support/test/espresso/assertion/ViewAssertions.html">{@code ViewAssertions}</a>
+        methods to check that the UI reflects the expected
+      state or behavior, after these user interactions are performed.
+      </li>
+    </ol>
+
+    <p>
+      These steps are covered in more detail in the sections below.
+    </p>
+
+    <p>
+      The following code snippet shows how your test class might invoke this basic workflow:
+    </p>
+
+<pre>
+onView(withId(R.id.my_view))            // withId(R.id.my_view) is a ViewMatcher
+        .perform(click())               // click() is a ViewAction
+        .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
+</pre>
+
+    <h3 id="espresso-aitc2">
+      Using Espresso with ActivityInstrumentationTestCase2
+    </h3>
+
+    <p>
+      If you are subclassing {@link android.test.ActivityInstrumentationTestCase2}
+      to create your Espresso test class, you must inject an
+      {@link android.app.Instrumentation} instance into your test class. This step is required in
+      order for your Espresso test to run with the
+      <a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">{@code AndroidJUnitRunner}</a>
+      test runner.
+    </p>
+
+    <p>
+      To do this, call the
+      {@link android.test.InstrumentationTestCase#injectInstrumentation(android.app.Instrumentation) injectInstrumentation()}
+      method and pass in the result of
+      <a href="{@docRoot}reference/android/support/test/InstrumentationRegistry.html#getInstrumentation()">
+      {@code InstrumentationRegistry.getInstrumentation()}</a>, as shown in the following code
+      example:
+    </p>
+
+<pre>
+import android.support.test.InstrumentationRegistry;
+
+public class MyEspressoTest
+        extends ActivityInstrumentationTestCase2&lt;MyActivity&gt; {
+
+    private MyActivity mActivity;
+
+    public MyEspressoTest() {
+        super(MyActivity.class);
+    }
+
+    &#64;Before
+    public void setUp() throws Exception {
+        super.setUp();
+        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
+        mActivity = getActivity();
+    }
+
+   ...
+}
+</pre>
+
+<p class="note"><strong>Note:</strong> Previously, {@link android.test.InstrumentationTestRunner}
+would inject the {@link android.app.Instrumentation} instance, but this test runner is being
+deprecated.</p>
+
+    <h3 id="accessing-ui-components">
+      Accessing UI Components
+    </h3>
+
+    <p>
+      Before Espresso can interact with the app under test, you must first specify the UI component
+      or <em>view</em>. Espresso supports the use of
+<a href="http://hamcrest.org/" class="external-link">Hamcrest matchers</a>
+      for specifying views and adapters in your app.
+    </p>
+
+    <p>
+      To find the view, call the <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onView(org.hamcrest.Matcher<android.view.View>)">
+      {@code onView()}</a>
+      method and pass in a view matcher that specifies the view that you are targeting. This is
+      described in more detail in <a href="#specifying-view-matcher">Specifying a View Matcher</a>.
+      The <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onView(org.hamcrest.Matcher<android.view.View>)">
+      {@code onView()}</a> method returns a
+      <a href="{@docRoot}reference/android/support/test/espresso/ViewInteraction.html">
+      {@code ViewInteraction}</a>
+      object that allows your test to interact with the view.
+      However, calling  the <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onView(org.hamcrest.Matcher<android.view.View>)">
+      {@code onView()}</a> method may not work if you want to locate a view in
+      an {@link android.widget.AdapterView} layout. In this case, follow the instructions in
+      <a href="#locating-adpeterview-view">Locating a view in an AdapterView</a> instead.
+    </p>
+
+    <p class="note">
+      <strong>Note</strong>: The <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onView(org.hamcrest.Matcher<android.view.View>)">
+      {@code onView()}</a> method does not check if the view you specified is
+      valid. Instead, Espresso searches only the current view hierarchy, using the matcher provided.
+      If no match is found, the method throws a
+      <a href="{@docRoot}reference/android/support/test/espresso/NoMatchingViewException.html">
+      {@code NoMatchingViewException}</a>.
+    </p>
+
+    <p>
+      The following code snippet shows how you might write a test that accesses an
+      {@link android.widget.EditText} field, enters a string of text, closes the virtual keyboard,
+      and then performs a button click.
+    </p>
+
+<pre>
+public void testChangeText_sameActivity() {
+    // Type text and then press the button.
+    onView(withId(R.id.editTextUserInput))
+            .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
+    onView(withId(R.id.changeTextButton)).perform(click());
+
+    // Check that the text was changed.
+    ...
+}
+</pre>
+
+    <h4 id="specifying-view-matcher">
+      Specifying a View Matcher
+    </h4>
+
+    <p>
+      You can specify a view matcher by using these approaches:
+    </p>
+
+    <ul>
+      <li>Calling methods in the
+        <a href="{@docRoot}reference/android/support/test/espresso/matcher/ViewMatchers.html">
+        {@code ViewMatchers}</a> class. For example, to find a view by looking for a text string it
+        displays, you can call a method like this:
+        <pre>
+onView(withText("Sign-in"));
+</pre>
+
+<p>Similarly you can call
+<a href="{@docRoot}reference/android/support/test/espresso/matcher/ViewMatchers.html#withId(int)">
+{@code withId()}</a> and providing the resource ID ({@code R.id}) of the view, as shown in the
+following example:</p>
+
+<pre>
+onView(withId(R.id.button_signin));
+</pre>
+
+    <p>
+      Android resource IDs are not guaranteed to be unique. If your test attempts to match to a
+      resource ID used by more than one view, Espresso throws an
+<a href="{@docRoot}reference/android/support/test/espresso/AmbiguousViewMatcherException.html">
+  {@code AmbiguousViewMatcherException}</a>.
+    </p>
+      </li>
+      <li>Using the Hamcrest
+      <a href="http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matchers.html"
+         class="external-link">{@code Matchers}</a> class. You can use the
+      {@code allOf()} methods to combine multiple matchers, such as
+      {@code containsString()} and {@code instanceOf()}. This approach allows you to
+      filter the match results more narrowly, as shown in the following example:
+<pre>
+onView(allOf(withId(R.id.button_signin), withText("Sign-in")));
+</pre>
+<p>You can use the {@code not} keyword to filter for views that don't correspond to the matcher, as
+shown in the following example:</p>
+<pre>
+onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));
+</pre>
+<p>To use these methods in your test, import the {@code org.hamcrest.Matchers} package. To
+learn more about Hamcrest matching, see the
+<a href="http://hamcrest.org/" class="external-link">Hamcrest site</a>.
+</p>
+      </li>
+    </ul>
+
+    <p>
+      To improve the performance of your Espresso tests, specify the minimum matching information
+      needed to find your target view. For example, if a view is uniquely identifiable by its
+      descriptive text, you do not need to specify that it is also assignable from the
+      {@link android.widget.TextView} instance.
+    </p>
+
+    <h4 id="#locating-adpeterview-view">
+      Locating a view in an AdapterView
+    </h4>
+
+    <p>
+      In an {@link android.widget.AdapterView} widget, the view is dynamically populated with child
+      views at runtime. If the target view you want to test is inside an
+      {@link android.widget.AdapterView}
+      (such as a {@link android.widget.ListView}, {@link android.widget.GridView}, or
+      {@link android.widget.Spinner}), the
+<a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onView(org.hamcrest.Matcher<android.view.View>)">
+  {@code onView()}</a> method might not work because only a
+      subset of the views may be loaded in the current view hierarchy.
+    </p>
+
+    <p>
+      Instead, call the <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher<java.lang.Object>)">{@code onData()}</a>
+      method to obtain a
+      <a href="{@docRoot}reference/android/support/test/espresso/DataInteraction.html">
+      {@code DataInteraction}</a>
+      object to access the target view element. Espresso handles loading the target view element
+      into the current view hierarchy. Espresso also takes care of scrolling to the target element,
+      and putting the element into focus.
+    </p>
+
+    <p class="note">
+      <strong>Note</strong>: The
+  <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher<java.lang.Object>)">{@code onData()}</a>
+      method does not check if if the item you specified corresponds with a view. Espresso searches
+      only the current view hierarchy. If no match is found, the method throws a
+      <a href="{@docRoot}reference/android/support/test/espresso/NoMatchingViewException.html">
+        {@code NoMatchingViewException}</a>.
+    </p>
+
+    <p>
+      The following code snippet shows how you can use the
+      <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher<java.lang.Object>)">{@code onData()}</a>
+      method together
+      with Hamcrest matching to search for a specific row in a list that contains a given string.
+      In this example, the {@code LongListActivity} class contains a list of strings exposed
+      through a {@link android.widget.SimpleAdapter}.
+    </p>
+
+<pre>
+onData(allOf(is(instanceOf(Map.class)),
+        hasEntry(equalTo(LongListActivity.ROW_TEXT), is(str))));
+</pre>
+
+    <h3 id="perform-actions">
+      Performing Actions
+    </h3>
+
+    <p>
+      Call the <a href="{@docRoot}reference/android/support/test/espresso/ViewInteraction.html#perform(android.support.test.espresso.ViewAction...)">{@code ViewInteraction.perform()}</a>
+      or
+      <a href="{@docRoot}reference/android/support/test/espresso/DataInteraction.html#perform(android.support.test.espresso.ViewAction...)">{@code DataInteraction.perform()}</a>
+      methods to
+      simulate user interactions on the UI component. You must pass in one or more
+      <a href="{@docRoot}reference/android/support/test/espresso/ViewAction.html">{@code ViewAction}</a>
+      objects as arguments. Espresso fires each action in sequence according to
+      the given order, and executes them in the main thread.
+    </p>
+
+    <p>
+      The
+      <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html">{@code ViewActions}</a>
+      class provides a list of helper methods for specifying common actions.
+      You can use these methods as convenient shortcuts instead of creating and configuring
+      individual <a href="{@docRoot}reference/android/support/test/espresso/ViewAction.html">{@code ViewAction}</a>
+      objects. You can specify such actions as:
+    </p>
+
+    <ul>
+      <li>
+       <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html#click()">{@code ViewActions.click()}</a>:
+       Clicks on the view.
+      </li>
+
+      <li>
+       <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html#typeText(java.lang.String)">{@code ViewActions.typeText()}</a>:
+       Clicks on a view and enters a specified string.
+      </li>
+
+      <li>
+       <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html#scrollTo()">{@code ViewActions.scrollTo()}</a>:
+       Scrolls to the view. The
+        target view must be subclassed from {@link android.widget.ScrollView}
+        and the value of its
+        <a href="http://developer.android.com/reference/android/view/View.html#attr_android:visibility">{@code android:visibility}</a>
+        property must be {@link android.view.View#VISIBLE}. For views that extend
+        {@link android.widget.AdapterView} (for example,
+        {@link android.widget.ListView}),
+        the
+        <a href="{@docRoot}reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher<java.lang.Object>)">{@code onData()}</a>
+        method takes care of scrolling for you.
+      </li>
+
+      <li>
+       <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html#pressKey(int)">{@code ViewActions.pressKey()}</a>:
+       Performs a key press using a specified keycode.
+      </li>
+
+      <li>
+      <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html#clearText()">{@code ViewActions.clearText()}</a>:
+      Clears the text in the target view.
+      </li>
+    </ul>
+
+    <p>
+      If the target view is inside a {@link android.widget.ScrollView}, perform the
+      <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html#scrollTo()">{@code ViewActions.scrollTo()}</a>
+      action first to display the view in the screen before other proceeding
+      with other actions. The
+      <a href="{@docRoot}reference/android/support/test/espresso/action/ViewActions.html#scrollTo()">{@code ViewActions.scrollTo()}</a>
+      action will have no effect if the view is already displayed.
+    </p>
+
+    <h3 id="verify-results">
+      Verifying Results
+    </h3>
+
+    <p>
+      Call the
+      <a href="{@docRoot}reference/android/support/test/espresso/ViewInteraction.html#check(android.support.test.espresso.ViewAssertion)">{@code ViewInteraction.check()}</a>
+      or
+      <a href="{@docRoot}reference/android/support/test/espresso/DataInteraction.html#check(android.support.test.espresso.ViewAssertion)">{@code DataInteraction.check()}</a>
+      method to assert
+      that the view in the UI matches some expected state. You must pass in a
+      <a href="{@docRoot}reference/android/support/test/espresso/ViewAssertion.html">
+      {@code ViewAssertion}</a> object as the argument. If the assertion fails, Espresso throws
+      an {@link junit.framework.AssertionFailedError}.
+    </p>
+
+    <p>
+      The
+  <a href="{@docRoot}reference/android/support/test/espresso/assertion/ViewAssertions.html">{@code ViewAssertions}</a>
+      class provides a list of helper methods for specifying common
+      assertions. The assertions you can use include:
+    </p>
+
+    <ul>
+      <li>
+        <a href="{@docRoot}reference/android/support/test/espresso/assertion/ViewAssertions.html#doesNotExist()">{@code doesNotExist}</a>:
+Asserts that there is no view matching the specified criteria in the current view hierarchy.
+      </li>
+
+      <li>
+        <a href="{@docRoot}reference/android/support/test/espresso/assertion/ViewAssertions.html#matches(org.hamcrest.Matcher&lt;? super android.view.View&gt;)">{@code matches}</a>:
+        Asserts that the specified view exists in the current view hierarchy
+        and its state matches some given Hamcrest matcher.
+      </li>
+
+      <li>
+       <a href="{@docRoot}reference/android/support/test/espresso/assertion/ViewAssertions.html#selectedDescendantsMatch(org.hamcrest.Matcher&lt;android.view.View&gt;, org.hamcrest.Matcher&lt;android.view.View&gt;)">{@code selectedDescendentsMatch}</a>
+       : Asserts that the specified children views for a
+        parent view exist, and their state matches some given Hamcrest matcher.
+      </li>
+    </ul>
+
+    <p>
+      The following code snippet shows how you might check that the text displayed in the UI has
+      the same value as the text previously entered in the
+      {@link android.widget.EditText} field.
+    </p>
+<pre>
+public void testChangeText_sameActivity() {
+    // Type text and then press the button.
+    ...
+
+    // Check that the text was changed.
+    onView(withId(R.id.textToBeChanged))
+            .check(matches(withText(STRING_TO_BE_TYPED)));
+}
+</pre>
+
+<h2 id="run">Run Espresso Tests on a Device or Emulator</h2>
+
+    <p>
+      To run Espresso tests, you must use the
+      <a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">{@code AndroidJUnitRunner}</a>
+      class provided in the
+      <a href="{@docRoot}tools/testing-support-library/index.html">
+      Android Testing Support Library</a> as your default test runner. The
+      <a href="{@docRoot}tools/building/plugin-for-gradle.html">Android Plug-in for
+      Gradle</a> provides a default directory ({@code src/androidTest/java}) for you to store the
+      instrumented test classes and test suites that you want to run on a device. The
+      plug-in compiles the test code in that directory and then executes the test app using
+      the configured test runner class.
+    </p>
+
+    <p>
+      To run Espresso tests in your Gradle project:
+    </p>
+
+    <ol>
+      <li>Specify
+        <a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">{@code AndroidJUnitRunner}</a>
+        as the default test instrumentation runner in
+      your {@code build.gradle} file:
+
+  <pre>
+android {
+    defaultConfig {
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+}</pre>
+      </li>
+      <li>Run your tests from the command-line by calling the the {@code connectedCheck}
+        (or {@code cC}) task:
+  <pre>
+./gradlew cC</pre>
+      </li>
+    </ol>
\ No newline at end of file
diff --git a/docs/html/training/testing/ui-testing/index.jd b/docs/html/training/testing/ui-testing/index.jd
new file mode 100644
index 0000000..605de22
--- /dev/null
+++ b/docs/html/training/testing/ui-testing/index.jd
@@ -0,0 +1,76 @@
+page.title=Automating User Interface Tests
+page.tags=testing
+
+trainingnavtop=true
+startpage=true
+
+@jd:body
+
+<div id="tb-wrapper">
+<div id="tb">
+        <h2>
+          You should also read
+        </h2>
+
+        <ul>
+          <li>
+            <a href="{@docRoot}/tools/testing-support-library/index.html">Testing Support Library</a>
+          </li>
+        </ul>
+</div>
+</div>
+
+<p>User interface (UI) testing lets you ensure that your app meets its functional requirements
+and achieves a high standard of quality such that it is more likely to be successfully adopted by
+users.</p>
+
+<p>One approach to UI testing is to simply have a human tester perform a set of user operations on
+the target app and verify that it is behaving correctly. However, this manual approach can be
+time-consuming, tedious, and error-prone. A more efficient approach is to write your UI
+tests such that user actions are performed in an automated way. The automated approach allows
+you to run your tests quickly and reliably in a repeatable manner.</p>
+
+<p class="note"><strong>Note: </strong>It is strongly encouraged that you use
+<a href="{@docRoot}sdk/installing/studio.html">Android Studio</a> for
+building your test apps, because it provides project setup, library inclusion, and packaging
+conveniences. This class assumes you are using Android Studio.</p>
+
+<p>To automate UI tests with Android Studio, you implement your test code in a separate
+Android test folder ({@code src/androidTest/java}). The
+<a href="{@docRoot}tools/building/plugin-for-gradle.html">Android
+Plug-in for Gradle</a> builds a test app based on your test code, then loads the test app on the
+same device as the target app. In your test code, you can use UI testing frameworks to
+simulate user interactions on the target app, in order to perform testing tasks that cover specific
+usage scenarios.</p>
+
+<p>For testing Android apps, you typically create these types of automated UI tests:</p>
+
+<ul>
+<li><em>UI tests that span a single app:</em> This type of test verifies that the target app behaves
+as expected when a user performs a specific action or enters a specific input in its activities.
+It allows you to check that the target app returns the correct UI output in response
+to user interactions in the app’s activities. UI testing frameworks like Espresso allow you to
+programmatically simulate user actions and test complex intra-app user interactions.</li>
+<li><em>UI tests that span multiple apps:</em> This type of test verifies the correct behavior of
+interactions between different user apps or between user apps and system apps. For example, you
+might want to test that your camera app shares images correctly with a 3rd-party social media app,
+or with the default Android Photos app. UI testing frameworks that support cross-app interactions,
+such as UI Automator, allow you to create tests for such scenarios.</li>
+</ul>
+
+<p>The lessons in this class teach you how to use the tools and APIs in the
+<a href="{@docRoot}/tools/testing-support-library/index.html">Android Testing Support Library</a>
+to build these types of automated tests. Before you begin building tests using these
+APIs, you must install the Android Testing Support Library, as described in
+<a href="{@docRoot}/tools/testing-support-library/index.html#setup">Downloading the Android
+Testing Support Library</a>.</p>
+
+<h2>Lessons</h2>
+<dl>
+  <dt><strong><a href="espresso-testing.html">
+Testing UI for a Single App</a></strong></dt>
+    <dd>Learn how to test UI in a single app by using the Espresso testing framework.</dd>
+  <dt><strong><a href="uiautomator-testing.html">
+Testing UI for Multiple Apps</a></strong></dt>
+    <dd>Learn how to test UI in multiple apps by using the UI Automator testing framework</dd>
+</dl>
\ No newline at end of file
diff --git a/docs/html/training/testing/ui-testing/uiautomator-testing.jd b/docs/html/training/testing/ui-testing/uiautomator-testing.jd
new file mode 100644
index 0000000..e314b70
--- /dev/null
+++ b/docs/html/training/testing/ui-testing/uiautomator-testing.jd
@@ -0,0 +1,520 @@
+page.title=Testing UI for Multiple Apps
+page.tags=testing,ui automator
+trainingnavtop=true
+
+@jd:body
+
+<!-- This is the training bar -->
+<div id="tb-wrapper">
+<div id="tb">
+  <h2>Dependencies and Prerequisites</h2>
+
+  <ul>
+    <li>Android 4.3 (API level 18) or higher</li>
+    <li><a href="{@docRoot}tools/testing-support-library/index.html">
+      Android Testing Support Library</a></li>
+  </ul>
+
+  <h2>This lesson teaches you to</h2>
+
+  <ol>
+    <li><a href="#setup">Set Up UI Automator</a></li>
+    <li><a href="#build">Create a UI Automator Test Class</a></li>
+    <li><a href="#run">Run UI Automator Tests on a Device or Emulator</a></li>
+  </ol>
+
+  <h2>You should also read</h2>
+
+  <ul>
+    <li><a href="{@docRoot}reference/android/support/test/package-summary.html">
+UI Automator API Reference</a></li>
+  </ul>
+
+  <h2>Try it out</h2>
+
+  <ul>
+    <li><a href="https://github.com/googlesamples/android-testing"
+class="external-link">UI Automator Code Samples</a></li>
+  </ul>
+</div>
+</div>
+
+<p>A user interface (UI) test that involves user interactions across multiple apps lets you
+verify that your app behaves correctly when the user flow crosses into other apps or into the
+system UI. An example of such a user flow is a messaging app that lets the user enter a text
+message, launches the Android contact picker so that the users can select recipients to send the
+message to, and then returns control to the original app for the user to submit the message.</p>
+
+<p>This lesson covers how to write such UI tests using the
+UI Automator testing framework provided by the
+<a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
+The UI Automator APIs let you interact with visible elements on a device, regardless of
+which {@link android.app.Activity} is in focus. Your test can look up a UI component by using
+convenient descriptors such as the text displayed in that component or its content description. UI
+Automator tests can run on devices running Android 4.3 (API level 18) or higher.</p>
+
+<p>The UI Automator testing framework is an instrumentation-based API and works
+with the
+<a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">
+  {@code AndroidJUnitRunner}</a>
+test runner.
+</p>
+
+<h2 id="setup">Set Up UI Automator</h2>
+<p>Before you begin using UI Automator, you must:</p>
+
+  <ul>
+      <li>
+        <strong>Install the Android Testing Support Library</strong>. The UI Automator API is
+        located under the {@code com.android.support.test.uiautomator} package. These classes allow
+        you to create tests that use the Espresso testing framework. To learn how to install the
+        library, see <a href="{@docRoot}tools/testing-support-library/index.html#setup">
+        Testing Support Library Setup</a>.
+      </li>
+
+      <li>
+        <strong>Set up your project structure.</strong> In your Gradle project, the source code for
+        the target app that you want to test is typically placed under the {@code app/src/main}
+        folder. The source code for instrumentation tests, including
+        your UI Automator tests, must be placed under the <code>app/src/androidTest</code> folder.
+        To learn more about setting up your project directory, see
+        <a href="{@docRoot}tools/projects/index.html">Managing Projects</a>.
+      </li>
+
+      <li>
+        <strong>Specify your Android testing dependencies</strong>. In order for the
+        <a href="{@docRoot}tools/building/plugin-for-gradle.html">Android Plug-in for Gradle</a> to
+        correctly build and run your UI Automator tests, you must specify the following libraries in
+        the {@code build.gradle} file of your Android app module:
+
+        <pre>
+dependencies {
+    androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
+    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.0.0'
+}
+</pre>
+      </li>
+  </ul>
+
+<p>To optimize your UI Automator testing, you should first inspect the target app’s UI components
+and ensure that they are accessible. These optimization tips are described in the next two
+sections.</p>
+
+<h3 id="inspecting-ui">Inspecting the UI on a device</h3>
+<p>Before designing your test, inspect the UI components that are visible on the device. To
+ensure that your UI Automator tests can access these components, check that these components
+have visible text labels,
+<a href="http://developer.android.com/reference/android/view/View.html#attr_android:contentDescription">
+{@code android:contentDescription}</a>
+values, or both.</p>
+
+<p>The {@code uiautomatorviewer} tool provides a convenient visual interface to inspect the layout
+hierarchy and view the properties of UI components that are visible on the foreground of the device.
+This information lets you create more fine-grained tests using UI Automator. For example, you can
+create a UI selector that matches a specific visible property. </p>
+
+<p>To launch the {@code uiautomatorviewer} tool:</p>
+
+<ol>
+  <li>Launch the target app on a physical device.</li>
+  <li>Connect the device to your development machine.</li>
+  <li>Open a terminal window and navigate to the {@code &lt;android-sdk&gt;/tools/} directory.</li>
+  <li>Run the tool with this command:
+<pre>$ uiautomatorviewer</pre>
+  </li>
+</ol>
+
+<p>To view the UI properties for your application:</p>
+
+<ol>
+  <li>In the {@code uiautomatorviewer} interface, click the <strong>Device Screenshot</strong>
+button.</li>
+  <li>Hover over the snapshot in the left-hand panel to see the UI components identified by the
+{@code uiautomatorviewertool}. The properties are listed in the lower right-hand panel and the
+layout hierarchy in the upper right-hand panel.</li>
+  <li>Optionally, click on the <strong>Toggle NAF Nodes</strong> button to see UI components that
+are non-accessible to UI Automator. Only limited information may be available for these
+components.</li>
+</ol>
+
+<p>To learn about the common types of UI components provided by Android, see
+<a href="{@docRoot}guide/topics/ui/index.html">User Interface</a>.</p>
+
+<h3>Ensuring your Activity is accessible</h3>
+<p>The UI Automator test framework depends on the accessibility features of the Android framework
+to look up individual UI elements. As a developer, you should implement these minimum
+optimizations in your {@link android.app.Activity} to support UI Automator:</p>
+
+<ul>
+<li>Use the
+<a href="{@docRoot}reference/android/view/View.html#attr_android:contentDescription">
+  {@code android:contentDescription}</a>
+attribute to label the {@link android.widget.ImageButton}, {@link android.widget.ImageView},
+{@link android.widget.CheckBox} and other user interface controls.</li>
+<li>Provide an <a href="{@docRoot}reference/android/widget/TextView.html#attr_android:hint">{@code android:hint}</a>
+attribute instead of a content description for {@link android.widget.EditText} fields.</li>
+<li>Associate an <a href="http://developer.android.com/reference/android/widget/TextView.html#attr_android:hint">
+  {@code android:hint}</a>
+attribute with any graphical icons used by controls that provide feedback to the user
+(for example, status or state information).</li>
+<li>Use the {@code uiautomatorviewer} tool to ensure that the UI component is accessible to the
+testing framework. You can also test the application by turning on accessibility services like
+TalkBack and Explore by Touch, and try using your application using only directional controls.</li>
+</ul>
+
+<p>Generally, app developers get accessibility support for free, courtesy of
+the {@link android.view.View} and {@link android.view.ViewGroup}
+classes. However, some apps use custom view elements to provide a richer user experience. Such
+custom elements won't get the accessibility support that is provided by the standard Android UI
+elements. If this applies to your app, make sure that it exposes the custom-drawn UI element to
+Android accessibility services by implementing the
+{@link android.view.accessibility.AccessibilityNodeProvider} class.</p>
+
+<p>If the custom view element contains a single element, make it accessible by
+<a href="{@docRoot}guide/topics/ui/accessibility/apps.html#accessibility-methods">implementing
+accessibility API methods</a>.
+If the custom view contains elements that are not views themselves (for example, a
+{@link android.webkit.WebView}, make sure it implements the
+{@link android.view.accessibility.AccessibilityNodeProvider} class. For container views that
+extend an existing container implementation
+(for example, a {@link android.widget.ListView}), implementing
+{@link android.view.accessibility.AccessibilityNodeProvider} is not necessary.</p>
+
+<p>For more information about implementing and testing accessibility, see
+<a href="{@docRoot}guide/topics/ui/accessibility/apps.html">Making Applications Accessible</a>.</p>
+
+<h2 id="build">Create a UI Automator Test Class</h2>
+
+<p>To build a UI Automator test, create a class that extends
+{@link android.test.InstrumentationTestCase}. Implement the following programming model in your
+UI Automator test class:</p>
+
+<ol>
+<li>Get a
+  <a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html">{@code UiDevice}</a>
+  object to access the device you want to test, by calling the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html#getInstance(android.app.Instrumentation)">
+{@code getInstance()}</a>
+method and passing it an {@link android.app.Instrumentation} object as the argument.</li>
+<li>Get a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+object to access a UI component that is displayed on the device
+	(for example, the current view in the foreground), by calling the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html#findObject(android.support.test.uiautomator.UiSelector)">
+  {@code findObject()}</a>
+method.
+</li>
+<li>Simulate a specific user interaction to perform on that UI component, by calling a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+method; for example, call
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#performMultiPointerGesture(android.view.MotionEvent.PointerCoords[]...)">
+  {@code performMultiPointerGesture()}</a>
+to simulate a multi-touch gesture, and
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#setText(java.lang.String)">{@code setText()}</a>
+to edit a text field. You can call on the APIs in steps 2 and 3 repeatedly as necessary to test
+more complex user interactions that involve multiple UI components or sequences of user actions.</li>
+<li>Check that the UI reflects the expected state or behavior, after these user interactions are
+	performed. </li>
+</ol>
+
+<p>These steps are covered in more detail in the sections below.</p>
+
+<h3 id="accessing-ui-components">Accessing UI Components</h3>
+<p>The
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html">{@code UiDevice}</a>
+  object is the primary way you access and manipulate the state of the
+device. In your tests, you can call
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html">{@code UiDevice}</a>
+methods to check for the state of various properties, such as current orientation or display size.
+Your test can use the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html">{@code UiDevice}</a>
+object to perform device-level actions, such as forcing the device into a specific rotation,
+pressing D-pad hardware buttons, and pressing the Home and Menu buttons.</p>
+
+<p>It’s good practice to start your test from the Home screen of the device. From the Home screen
+(or some other starting location you’ve chosen in the device), you can call the methods provided by
+the UI Automator API to select and interact with specific UI elements. </p>
+
+<p>The following code snippet shows how your test might get an instance of
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html">{@code UiDevice}</a>
+and simulate a Home button press:</p>
+
+<pre>
+import android.test.InstrumentationTestCase;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.By;
+
+public class CalculatorUiTest extends InstrumentationTestCase {
+
+    private UiDevice mDevice;
+
+    public void setUp() {
+        // Initialize UiDevice instance
+        mDevice = UiDevice.getInstance(getInstrumentation());
+
+        // Start from the home screen
+        mDevice.pressHome();
+        mDevice.wait(Until.hasObject(By.pkg(getHomeScreenPackage()).depth(0)),
+    }
+}
+</pre>
+
+<p>Use the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiDevice.html#findObject(android.support.test.uiautomator.UiSelector)">{@code findObject()}</a>
+method to retrieve a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+which represents a view that matches a given selector criteria. You can reuse the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+instances that you have created in other parts of your app testing, as needed. Note that the
+UI Automator test framework searches the current display for a match every time your test uses a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+instance to click on a UI element or query a property.</p>
+
+<p>The following snippet shows how your test might construct
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+instances that represent a Cancel button and a OK button in an app.</p>
+
+<pre>
+UiObject cancelButton = mDevice.findObject(new UiSelector()
+        .text("Cancel"))
+        .className("android.widget.Button"));
+UiObject okButton = mDevice.findObject(new UiSelector()
+        .text("OK"))
+        .className("android.widget.Button"));
+
+// Simulate a user-click on the OK button, if found.
+if(okButton.exists() &#38;&#38; okButton.isEnabled()) {
+    okButton.click();
+}
+</pre>
+
+<h4 id="specifying-selector">Specifying a selector</h4>
+<p>If you want to access a specific UI component in an app, use the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiSelector.html">{@code UiSelector}</a>
+class. This class represents a query for specific elements in the
+currently displayed UI. </p>
+
+<p>If more than one matching element is found, the first matching element in the layout hierarchy
+is returned as the target
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>.
+When constructing a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiSelector.html">{@code UiSelector}</a>,
+you can chain together multiple properties to refine your search. If no matching UI element is
+found, a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObjectNotFoundException.html">
+{@code UiAutomatorObjectNotFoundException}</a> is thrown. </p>
+
+<p>You can use the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiSelector.html#childSelector(android.support.test.uiautomator.UiSelector)">{@code childSelector()}</a>
+method to nest multiple
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiSelector.html">{@code UiSelector}</a>
+instances. For example, the following code example shows how your test might specify a search to
+find the first {@link android.widget.ListView} in the currently displayed UI, then search within that
+{@link android.widget.ListView} to find a UI element with the text property Apps.</p>
+
+<pre>
+UiObject appItem = new UiObject(new UiSelector()
+        .className("android.widget.ListView")
+        .instance(1)
+        .childSelector(new UiSelector()
+        .text("Apps")));
+</pre>
+
+<p>As a best practice, when specifying a selector, you should use a Resource ID (if one is assigned
+to a UI element) instead of a text element or content-descriptor. Not all elements have a text
+element (for example, icons in a toolbar). Text selectors are brittle and can lead to test failures
+if there are minor changes to the UI. They may also not scale across different languages; your text
+selectors may not match translated strings.</p>
+
+<p>It can be useful to specify the object state in your selector criteria. For example, if you want
+to select a list of all checked elements so that you can uncheck them, call the
+<a href="{@docRoot}reference/android/support/test/uiautomator/By.html#checked(boolean)">
+{@code checked()}</a>
+method with the argument set to {@code true}.</p>
+
+<h3 id="performing-actions">Performing Actions</h3>
+
+<p>Once your test has obtained a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+object, you can call the methods in the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>
+class to perform user interactions on the UI component represented by that
+object. You can specify such actions as:</p>
+
+<ul>
+<li>
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#click()">
+  {@code click()}</a>
+: Clicks the center of the visible bounds of the UI element.</li>
+<li>
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#dragTo(int, int, int)">
+  {@code dragTo()}</a>
+: Drags this object to arbitrary coordinates.</li>
+<li>
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#setText(java.lang.String)">
+  {@code setText()}</a>
+: Sets the text in an editable field, after clearing the field's content.
+Conversely, the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#clearTextField()">
+  {@code clearTextField()}</a>
+method clears the existing text in an editable field.</li>
+<li>
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#swipeUp(int)">
+  {@code swipeUp()}</a>
+: Performs the swipe up action on the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html">{@code UiObject}</a>.
+Similarly, the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#swipeDown(int)">
+  {@code swipeDown()}</a>,
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#swipeLeft(int)">
+  {@code swipeLeft()}</a>, and
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiObject.html#swipeRight(int)">
+  {@code swipeRight()}</a>
+methods perform corresponding actions.</li>
+</ul>
+
+<p>The UI Automator testing framework allows you to send an
+{@link android.content.Intent}
+or launch an {@link android.app.Activity}
+without using shell commands, by getting a
+{@link android.content.Context}
+object through
+{@link android.app.Instrumentation#getContext() getContext()}.</p>
+
+<p>The following snippet shows how your test can use an
+{@link android.content.Intent} to launch the app under test. This approach is useful when you are
+only interested in testing the calculator app, and don't care about the launcher.</p>
+
+<pre>
+public void setUp() {
+    ...
+
+    // Launch a simple calculator app
+    Context context = getInstrumentation().getContext();
+    Intent intent = context.getPackageManager()
+            .getLaunchIntentForPackage(CALC_PACKAGE);
+    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            // Clear out any previous instances
+    context.startActivity(intent);
+    mDevice.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT);
+}
+</pre>
+
+<h4 id="actions-on-collections">Performing actions on collections</h4>
+
+<p>Use the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiCollection.html">
+  {@code UiCollection}</a>
+class if you want to simulate user interactions on a
+collection of items (for example, songs in a music album or a list of emails in an Inbox). To
+create a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiCollection.html">
+  {@code UiCollection}</a>
+object, specify a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiSelector.html">{@code UiSelector}</a>
+that searches for a
+UI container or a wrapper of other child UI elements, such as a layout view that contains child UI
+elements.</p>
+
+<p>The following code snippet shows how your test might construct a
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiCollection.html">
+  {@code UiCollection}</a>
+to represent a video album that is displayed within a {@link android.widget.FrameLayout}:</p>
+
+<pre>
+UiCollection videos = new UiCollection(new UiSelector()
+        .className("android.widget.FrameLayout"));
+
+// Retrieve the number of videos in this collection:
+int count = videos.getChildCount(new UiSelector()
+        .className("android.widget.LinearLayout"));
+
+// Find a specific video and simulate a user-click on it
+UiObject video = videos.getChildByText(new UiSelector()
+        .className("android.widget.LinearLayout"), "Cute Baby Laughing");
+video.click();
+
+// Simulate selecting a checkbox that is associated with the video
+UiObject checkBox = video.getChild(new UiSelector()
+        .className("android.widget.Checkbox"));
+if(!checkBox.isSelected()) checkbox.click();
+</pre>
+
+<h4 id="actions-on-scrollable-views">Performing actions on scrollable views</h4>
+<p>Use the
+<a href="{@docRoot}reference/android/support/test/uiautomator/UiScrollable.html">
+  {@code UiScrollable}</a>
+class to simulate vertical or horizontal scrolling across a display. This technique is helpful when
+a UI element is positioned off-screen and you need to scroll to bring it into view.</p>
+
+<p>The following code snippet shows how to simulate scrolling down the Settings menu and clicking
+on an About tablet option:</p>
+
+<pre>
+UiScrollable settingsItem = new UiScrollable(new UiSelector()
+        .className("android.widget.ListView"));
+UiObject about = settingsItem.getChildByText(new UiSelector()
+        .className("android.widget.LinearLayout"), "About tablet");
+about.click();
+</pre>
+
+<h3 id="verifying-results">Verifying Results</h3>
+<p>The {@link android.test.InstrumentationTestCase} extends {@link junit.framework.TestCase}, so
+you can use standard JUnit <a href="http://junit.org/javadoc/latest/org/junit/Assert.html"
+class="external-link">{@code Assert}</a> methods to test
+that UI components in the app return the expected results. </p>
+
+<p>The following snippet shows how your test can locate several buttons in a calculator app, click
+on them in order, then verify that the correct result is displayed.</p>
+
+<pre>
+private static final String CALC_PACKAGE = "com.myexample.calc";
+
+public void testTwoPlusThreeEqualsFive() {
+    // Enter an equation: 2 + 3 = ?
+    mDevice.findObject(new UiSelector()
+            .packageName(CALC_PACKAGE).resourceId("two")).click();
+    mDevice.findObject(new UiSelector()
+            .packageName(CALC_PACKAGE).resourceId("plus")).click();
+    mDevice.findObject(new UiSelector()
+            .packageName(CALC_PACKAGE).resourceId("three")).click();
+    mDevice.findObject(new UiSelector()
+            .packageName(CALC_PACKAGE).resourceId("equals")).click();
+
+    // Verify the result = 5
+    UiObject result = mDevice.findObject(By.res(CALC_PACKAGE, "result"));
+    assertEquals("5", result.getText());
+}
+</pre>
+
+<h2 id="run">Run UI Automator Tests on a Device or Emulator</h2>
+<p>UI Automator tests are based on the {@link android.app.Instrumentation} class. The
+<a href="https://developer.android.com/tools/building/plugin-for-gradle.html">
+  Android Plug-in for Gradle</a>
+provides a default directory ({@code src/androidTest/java}) for you to store the instrumented test
+classes and test suites that you want to run on a device. The plug-in compiles the test
+code in that directory and then executes the test app using a test runner class. You are
+strongly encouraged to use the
+<a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">{@code AndroidJUnitRunner}</a>
+class provided in the
+<a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>
+as your default test runner. </p>
+
+<p>To run UI Automator tests in your Gradle project:</p>
+
+<ol>
+<li>Specify
+<a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">{@code AndroidJUnitRunner}</a>
+as the default test instrumentation runner in your {@code build.gradle} file:
+<pre>
+android {
+    defaultConfig {
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+}</pre>
+</li>
+<li>Run your tests from the command-line by calling the {@code connectedCheck}
+  (or {@code cC}) task:
+<pre>./gradlew cC</pre>
+</li>
+</ol>
\ No newline at end of file
diff --git a/docs/html/training/training_toc.cs b/docs/html/training/training_toc.cs
index 2873b5b..3ee7ab7 100644
--- a/docs/html/training/training_toc.cs
+++ b/docs/html/training/training_toc.cs
@@ -1840,6 +1840,24 @@
         </ul>
       </li>
     </ul>
+    <ul>
+      <li class="nav-section">
+      <div class="nav-section-header"><a href="<?cs var:toroot ?>training/testing/ui-testing/index.html"
+         description="How to automate your user interface tests for Android apps.">
+            Automating UI Tests
+          </a></div>
+        <ul>
+          <li><a href="<?cs var:toroot ?>training/testing/ui-testing/espresso-testing.html">
+            <span class="en">Testing UI for a Single App</span>
+          </a>
+          </li>
+          <li><a href="<?cs var:toroot ?>training/testing/ui-testing/uiautomator-testing.html">
+            <span class="en">Testing UI for Multiple Apps</span>
+          </a>
+          </li>
+        </ul>
+      </li>
+    </ul>
   </li>
   <!-- End best Testing -->
 
diff --git a/libs/hwui/FontRenderer.cpp b/libs/hwui/FontRenderer.cpp
index 6bef7c7..56380db 100644
--- a/libs/hwui/FontRenderer.cpp
+++ b/libs/hwui/FontRenderer.cpp
@@ -46,51 +46,11 @@
 // blur inputs smaller than this constant will bypass renderscript
 #define RS_MIN_INPUT_CUTOFF 10000
 
-#define USE_GLOPS true
-
 ///////////////////////////////////////////////////////////////////////////////
 // TextSetupFunctor
 ///////////////////////////////////////////////////////////////////////////////
-void TextSetupFunctor::setup(GLenum glyphFormat) {
-    renderer->setupDraw();
-    renderer->setupDrawTextGamma(paint);
-    renderer->setupDrawDirtyRegionsDisabled();
-    renderer->setupDrawWithTexture(glyphFormat == GL_ALPHA);
-    switch (glyphFormat) {
-        case GL_ALPHA: {
-            renderer->setupDrawAlpha8Color(paint->getColor(), alpha);
-            break;
-        }
-        case GL_RGBA: {
-            float floatAlpha = alpha / 255.0f;
-            renderer->setupDrawColor(floatAlpha, floatAlpha, floatAlpha, floatAlpha);
-            break;
-        }
-        default: {
-#if DEBUG_FONT_RENDERER
-            ALOGD("TextSetupFunctor: called with unknown glyph format %x", glyphFormat);
-#endif
-            break;
-        }
-    }
-    renderer->setupDrawColorFilter(paint->getColorFilter());
-    renderer->setupDrawShader(paint->getShader());
-    renderer->setupDrawBlending(paint);
-    renderer->setupDrawProgram();
-    renderer->setupDrawModelView(kModelViewMode_Translate, false,
-            0.0f, 0.0f, 0.0f, 0.0f, pureTranslate);
-    // Calling setupDrawTexture with the name 0 will enable the
-    // uv attributes and increase the texture unit count
-    // texture binding will be performed by the font renderer as
-    // needed
-    renderer->setupDrawTexture(0);
-    renderer->setupDrawPureColorUniforms();
-    renderer->setupDrawColorFilterUniforms(paint->getColorFilter());
-    renderer->setupDrawShaderUniforms(paint->getShader(), pureTranslate);
-    renderer->setupDrawTextGammaUniforms();
-}
 
-void TextSetupFunctor::draw(CacheTexture& texture, bool linearFiltering) {
+void TextDrawFunctor::draw(CacheTexture& texture, bool linearFiltering) {
     int textureFillFlags = static_cast<int>(texture.getFormat() == GL_ALPHA
             ? TextureFillFlags::kIsAlphaMaskTexture : TextureFillFlags::kNone);
     if (linearFiltering) {
@@ -508,11 +468,6 @@
 void FontRenderer::issueDrawCommand(Vector<CacheTexture*>& cacheTextures) {
     if (!mFunctor) return;
 
-#if !USE_GLOPS
-    Caches& caches = mFunctor->renderer->getCaches();
-    RenderState& renderState = mFunctor->renderer->renderState();
-#endif
-
     bool first = true;
     bool forceRebind = false;
     for (uint32_t i = 0; i < cacheTextures.size(); i++) {
@@ -520,37 +475,12 @@
         if (texture->canDraw()) {
             if (first) {
                 checkTextureUpdate();
-#if !USE_GLOPS
-                mFunctor->setup(texture->getFormat());
-
-                renderState.meshState().bindQuadIndicesBuffer();
-
-                // If returns true, a VBO was bound and we must
-                // rebind our vertex attrib pointers even if
-                // they have the same values as the current pointers
-                forceRebind = renderState.meshState().unbindMeshBuffer();
-
-                caches.textureState().activateTexture(0);
-#endif
                 first = false;
                 mDrawn = true;
             }
-#if USE_GLOPS
+
             mFunctor->draw(*texture, mLinearFiltering);
-#endif
 
-#if !USE_GLOPS
-            caches.textureState().bindTexture(texture->getTextureId());
-            texture->setLinearFiltering(mLinearFiltering);
-
-            TextureVertex* mesh = texture->mesh();
-            MeshState& meshState = renderState.meshState();
-            meshState.bindPositionVertexPointer(forceRebind, &mesh[0].x);
-            meshState.bindTexCoordsVertexPointer(forceRebind, &mesh[0].u);
-
-            glDrawElements(GL_TRIANGLES, texture->meshElementCount(),
-                    GL_UNSIGNED_SHORT, texture->indices());
-#endif
             texture->resetMesh();
             forceRebind = false;
         }
@@ -689,7 +619,7 @@
     return image;
 }
 
-void FontRenderer::initRender(const Rect* clip, Rect* bounds, TextSetupFunctor* functor) {
+void FontRenderer::initRender(const Rect* clip, Rect* bounds, TextDrawFunctor* functor) {
     checkInit();
 
     mDrawn = false;
@@ -717,7 +647,7 @@
 
 bool FontRenderer::renderPosText(const SkPaint* paint, const Rect* clip, const char *text,
         uint32_t startIndex, uint32_t len, int numGlyphs, int x, int y,
-        const float* positions, Rect* bounds, TextSetupFunctor* functor, bool forceFinish) {
+        const float* positions, Rect* bounds, TextDrawFunctor* functor, bool forceFinish) {
     if (!mCurrentFont) {
         ALOGE("No font set");
         return false;
@@ -735,7 +665,7 @@
 
 bool FontRenderer::renderTextOnPath(const SkPaint* paint, const Rect* clip, const char *text,
         uint32_t startIndex, uint32_t len, int numGlyphs, const SkPath* path,
-        float hOffset, float vOffset, Rect* bounds, TextSetupFunctor* functor) {
+        float hOffset, float vOffset, Rect* bounds, TextDrawFunctor* functor) {
     if (!mCurrentFont) {
         ALOGE("No font set");
         return false;
diff --git a/libs/hwui/FontRenderer.h b/libs/hwui/FontRenderer.h
index 0603389..dfb107c 100644
--- a/libs/hwui/FontRenderer.h
+++ b/libs/hwui/FontRenderer.h
@@ -46,9 +46,9 @@
 
 class OpenGLRenderer;
 
-class TextSetupFunctor {
+class TextDrawFunctor {
 public:
-    TextSetupFunctor(OpenGLRenderer* renderer, float x, float y, bool pureTranslate,
+    TextDrawFunctor(OpenGLRenderer* renderer, float x, float y, bool pureTranslate,
             int alpha, SkXfermode::Mode mode, const SkPaint* paint)
         : renderer(renderer)
         , x(x)
@@ -59,8 +59,6 @@
         , paint(paint) {
     }
 
-    void setup(GLenum glyphFormat);
-
     void draw(CacheTexture& texture, bool linearFiltering);
 
     OpenGLRenderer* renderer;
@@ -92,12 +90,12 @@
     // bounds is an out parameter
     bool renderPosText(const SkPaint* paint, const Rect* clip, const char *text,
             uint32_t startIndex, uint32_t len, int numGlyphs, int x, int y, const float* positions,
-            Rect* bounds, TextSetupFunctor* functor, bool forceFinish = true);
+            Rect* bounds, TextDrawFunctor* functor, bool forceFinish = true);
 
     // bounds is an out parameter
     bool renderTextOnPath(const SkPaint* paint, const Rect* clip, const char *text,
             uint32_t startIndex, uint32_t len, int numGlyphs, const SkPath* path,
-            float hOffset, float vOffset, Rect* bounds, TextSetupFunctor* functor);
+            float hOffset, float vOffset, Rect* bounds, TextDrawFunctor* functor);
 
     struct DropShadow {
         uint32_t width;
@@ -135,7 +133,7 @@
     void flushAllAndInvalidate();
 
     void checkInit();
-    void initRender(const Rect* clip, Rect* bounds, TextSetupFunctor* functor);
+    void initRender(const Rect* clip, Rect* bounds, TextDrawFunctor* functor);
     void finishRender();
 
     void issueDrawCommand(Vector<CacheTexture*>& cacheTextures);
@@ -176,7 +174,7 @@
 
     bool mUploadTexture;
 
-    TextSetupFunctor* mFunctor;
+    TextDrawFunctor* mFunctor;
     const Rect* mClip;
     Rect* mBounds;
     bool mDrawn;
diff --git a/libs/hwui/GlopBuilder.cpp b/libs/hwui/GlopBuilder.cpp
index 0a46014..9ca6bc6 100644
--- a/libs/hwui/GlopBuilder.cpp
+++ b/libs/hwui/GlopBuilder.cpp
@@ -586,6 +586,12 @@
     mDescription.hasColors = mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::kColor;
     mDescription.hasVertexAlpha = mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::kAlpha;
 
+    // Enable debug highlight when what we're about to draw is tested against
+    // the stencil buffer and if stencil highlight debugging is on
+    mDescription.hasDebugHighlight = !mCaches.debugOverdraw
+            && mCaches.debugStencilClip == Caches::kStencilShowHighlight
+            && mRenderState.stencil().isTestEnabled();
+
     // serialize shader info into ShaderData
     GLuint textureUnit = mOutGlop->fill.texture.texture ? 1 : 0;
     SkiaShader::store(mCaches, mShader, mOutGlop->transform.modelView,
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index 02fbd89..aa722d0 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -57,8 +57,6 @@
     #define EVENT_LOGD(...)
 #endif
 
-#define USE_GLOPS true
-
 namespace android {
 namespace uirenderer {
 
@@ -82,8 +80,6 @@
     // *set* draw modifiers to be 0
     memset(&mDrawModifiers, 0, sizeof(mDrawModifiers));
     mDrawModifiers.mOverrideLayerAlpha = 1.0f;
-
-    memcpy(mMeshVertices, kUnitQuadVertices, sizeof(kUnitQuadVertices));
 }
 
 OpenGLRenderer::~OpenGLRenderer() {
@@ -846,124 +842,42 @@
 }
 
 void OpenGLRenderer::drawTextureLayer(Layer* layer, const Rect& rect) {
-    if (USE_GLOPS) {
-        bool snap = !layer->getForceFilter()
-                && layer->getWidth() == (uint32_t) rect.getWidth()
-                && layer->getHeight() == (uint32_t) rect.getHeight();
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedUvQuad(nullptr, Rect(0, 1, 1, 0)) // TODO: simplify with VBO
-                .setFillTextureLayer(*layer, getLayerAlpha(layer))
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewMapUnitToRectOptionalSnap(snap, rect)
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    float alpha = getLayerAlpha(layer);
-    setupDraw();
-    if (layer->getRenderTarget() == GL_TEXTURE_2D) {
-        setupDrawWithTexture();
-    } else {
-        setupDrawWithExternalTexture();
-    }
-    setupDrawTextureTransform();
-    setupDrawColor(alpha, alpha, alpha, alpha);
-    setupDrawColorFilter(layer->getColorFilter());
-    setupDrawBlending(layer);
-    setupDrawProgram();
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(layer->getColorFilter());
-    if (layer->getRenderTarget() == GL_TEXTURE_2D) {
-        setupDrawTexture(layer->getTextureId());
-    } else {
-        setupDrawExternalTexture(layer->getTextureId());
-    }
-    if (currentTransform()->isPureTranslate()
-            && !layer->getForceFilter()
+    bool snap = !layer->getForceFilter()
             && layer->getWidth() == (uint32_t) rect.getWidth()
-            && layer->getHeight() == (uint32_t) rect.getHeight()) {
-        const float x = floorf(rect.left + currentTransform()->getTranslateX() + 0.5f);
-        const float y = floorf(rect.top + currentTransform()->getTranslateY() + 0.5f);
-
-        layer->setFilter(GL_NEAREST);
-        setupDrawModelView(kModelViewMode_TranslateAndScale, false,
-                x, y, x + rect.getWidth(), y + rect.getHeight(), true);
-    } else {
-        layer->setFilter(GL_LINEAR);
-        setupDrawModelView(kModelViewMode_TranslateAndScale, false,
-                rect.left, rect.top, rect.right, rect.bottom);
-    }
-    setupDrawTextureTransformUniforms(layer->getTexTransform());
-    setupDrawMesh(&mMeshVertices[0].x, &mMeshVertices[0].u);
-
-    glDrawArrays(GL_TRIANGLE_STRIP, 0, kUnitQuadCount);
+            && layer->getHeight() == (uint32_t) rect.getHeight();
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedUvQuad(nullptr, Rect(0, 1, 1, 0)) // TODO: simplify with VBO
+            .setFillTextureLayer(*layer, getLayerAlpha(layer))
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewMapUnitToRectOptionalSnap(snap, rect)
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::composeLayerRect(Layer* layer, const Rect& rect, bool swap) {
     if (layer->isTextureLayer()) {
         EVENT_LOGD("composeTextureLayerRect");
-        resetDrawTextureTexCoords(0.0f, 1.0f, 1.0f, 0.0f);
         drawTextureLayer(layer, rect);
-        resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f);
     } else {
         EVENT_LOGD("composeHardwareLayerRect");
 
-        if (USE_GLOPS) {
-            Blend::ModeOrderSwap modeUsage = swap ?
-                    Blend::ModeOrderSwap::Swap : Blend::ModeOrderSwap::NoSwap;
-            const Matrix4& transform = swap ? Matrix4::identity() : *currentTransform();
-            bool snap = !swap
-                    && layer->getWidth() == static_cast<uint32_t>(rect.getWidth())
-                    && layer->getHeight() == static_cast<uint32_t>(rect.getHeight());
-            Glop glop;
-            GlopBuilder(mRenderState, mCaches, &glop)
-                    .setMeshTexturedUvQuad(nullptr, layer->texCoords)
-                    .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), modeUsage)
-                    .setTransform(currentSnapshot()->getOrthoMatrix(), transform, false)
-                    .setModelViewMapUnitToRectOptionalSnap(snap, rect)
-                    .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                    .build();
-            renderGlop(glop);
-            return;
-        }
-
-        const Rect& texCoords = layer->texCoords;
-        resetDrawTextureTexCoords(texCoords.left, texCoords.top,
-                texCoords.right, texCoords.bottom);
-
-        float x = rect.left;
-        float y = rect.top;
-        bool simpleTransform = currentTransform()->isPureTranslate()
-                && layer->getWidth() == (uint32_t) rect.getWidth()
-                && layer->getHeight() == (uint32_t) rect.getHeight();
-
-        if (simpleTransform) {
-            // When we're swapping, the layer is already in screen coordinates
-            if (!swap) {
-                x = floorf(rect.left + currentTransform()->getTranslateX() + 0.5f);
-                y = floorf(rect.top + currentTransform()->getTranslateY() + 0.5f);
-            }
-
-            layer->setFilter(GL_NEAREST, true);
-        } else {
-            layer->setFilter(GL_LINEAR, true);
-        }
-
-        SkPaint layerPaint;
-        layerPaint.setAlpha(getLayerAlpha(layer) * 255);
-        layerPaint.setXfermodeMode(layer->getMode());
-        layerPaint.setColorFilter(layer->getColorFilter());
-
-        bool blend = layer->isBlend() || getLayerAlpha(layer) < 1.0f;
-        drawTextureMesh(x, y, x + rect.getWidth(), y + rect.getHeight(),
-                layer->getTextureId(), &layerPaint, blend,
-                &mMeshVertices[0].x, &mMeshVertices[0].u,
-                GL_TRIANGLE_STRIP, kUnitQuadCount, swap, swap || simpleTransform);
-
-        resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f);
+        Blend::ModeOrderSwap modeUsage = swap ?
+                Blend::ModeOrderSwap::Swap : Blend::ModeOrderSwap::NoSwap;
+        const Matrix4& transform = swap ? Matrix4::identity() : *currentTransform();
+        bool snap = !swap
+                && layer->getWidth() == static_cast<uint32_t>(rect.getWidth())
+                && layer->getHeight() == static_cast<uint32_t>(rect.getHeight());
+        Glop glop;
+        GlopBuilder(mRenderState, mCaches, &glop)
+                .setMeshTexturedUvQuad(nullptr, layer->texCoords)
+                .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), modeUsage)
+                .setTransform(currentSnapshot()->getOrthoMatrix(), transform, false)
+                .setModelViewMapUnitToRectOptionalSnap(snap, rect)
+                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+                .build();
+        renderGlop(glop);
     }
 }
 
@@ -1079,69 +993,8 @@
     const float texY = 1.0f / float(layer->getHeight());
     const float height = rect.getHeight();
 
-    if (USE_GLOPS) {
-        TextureVertex quadVertices[count * 4];
-        //std::unique_ptr<TextureVertex[]> quadVertices(new TextureVertex[count * 4]);
-        TextureVertex* mesh = &quadVertices[0];
-        for (size_t i = 0; i < count; i++) {
-            const android::Rect* r = &rects[i];
-
-            const float u1 = r->left * texX;
-            const float v1 = (height - r->top) * texY;
-            const float u2 = r->right * texX;
-            const float v2 = (height - r->bottom) * texY;
-
-            // TODO: Reject quads outside of the clip
-            TextureVertex::set(mesh++, r->left, r->top, u1, v1);
-            TextureVertex::set(mesh++, r->right, r->top, u2, v1);
-            TextureVertex::set(mesh++, r->left, r->bottom, u1, v2);
-            TextureVertex::set(mesh++, r->right, r->bottom, u2, v2);
-        }
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedIndexedQuads(&quadVertices[0], count * 6)
-                .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), Blend::ModeOrderSwap::NoSwap)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewOffsetRectSnap(0, 0, rect)
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        DRAW_DOUBLE_STENCIL_IF(!layer->hasDrawnSinceUpdate, renderGlop(glop));
-        return;
-    }
-
-    const float alpha = getLayerAlpha(layer);
-
-    setupDraw();
-
-    // We must get (and therefore bind) the region mesh buffer
-    // after we setup drawing in case we need to mess with the
-    // stencil buffer in setupDraw()
-    TextureVertex* mesh = mCaches.getRegionMesh();
-    uint32_t numQuads = 0;
-
-    setupDrawWithTexture();
-    setupDrawColor(alpha, alpha, alpha, alpha);
-    setupDrawColorFilter(layer->getColorFilter());
-    setupDrawBlending(layer);
-    setupDrawProgram();
-    setupDrawDirtyRegionsDisabled();
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(layer->getColorFilter());
-    setupDrawTexture(layer->getTextureId());
-    if (currentTransform()->isPureTranslate()) {
-        const float x = floorf(rect.left + currentTransform()->getTranslateX() + 0.5f);
-        const float y = floorf(rect.top + currentTransform()->getTranslateY() + 0.5f);
-
-        layer->setFilter(GL_NEAREST);
-        setupDrawModelView(kModelViewMode_Translate, false,
-                x, y, x + rect.getWidth(), y + rect.getHeight(), true);
-    } else {
-        layer->setFilter(GL_LINEAR);
-        setupDrawModelView(kModelViewMode_Translate, false,
-                rect.left, rect.top, rect.right, rect.bottom);
-    }
-    setupDrawMeshIndices(&mesh[0].x, &mesh[0].u);
-
+    TextureVertex quadVertices[count * 4];
+    TextureVertex* mesh = &quadVertices[0];
     for (size_t i = 0; i < count; i++) {
         const android::Rect* r = &rects[i];
 
@@ -1155,21 +1008,16 @@
         TextureVertex::set(mesh++, r->right, r->top, u2, v1);
         TextureVertex::set(mesh++, r->left, r->bottom, u1, v2);
         TextureVertex::set(mesh++, r->right, r->bottom, u2, v2);
-
-        numQuads++;
-
-        if (numQuads >= kMaxNumberOfQuads) {
-            DRAW_DOUBLE_STENCIL(glDrawElements(GL_TRIANGLES, numQuads * 6,
-                    GL_UNSIGNED_SHORT, nullptr));
-            numQuads = 0;
-            mesh = mCaches.getRegionMesh();
-        }
     }
-
-    if (numQuads > 0) {
-        DRAW_DOUBLE_STENCIL(glDrawElements(GL_TRIANGLES, numQuads * 6,
-                GL_UNSIGNED_SHORT, nullptr));
-    }
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedIndexedQuads(&quadVertices[0], count * 6)
+            .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), Blend::ModeOrderSwap::NoSwap)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewOffsetRectSnap(0, 0, rect)
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    DRAW_DOUBLE_STENCIL_IF(!layer->hasDrawnSinceUpdate, renderGlop(glop));
 
 #if DEBUG_LAYERS_AS_REGIONS
     drawRegionRectsDebug(layer->region);
@@ -1248,21 +1096,6 @@
     }
 }
 
-void OpenGLRenderer::issueIndexedQuadDraw(Vertex* mesh, GLsizei quadsCount) {
-    GLsizei elementsCount = quadsCount * 6;
-    while (elementsCount > 0) {
-        GLsizei drawCount = MathUtils::min(elementsCount, (GLsizei) kMaxNumberOfQuads * 6);
-
-        setupDrawIndexedVertices(&mesh[0].x);
-        glDrawElements(GL_TRIANGLES, drawCount, GL_UNSIGNED_SHORT, nullptr);
-
-        elementsCount -= drawCount;
-        // Though there are 4 vertices in a quad, we use 6 indices per
-        // quad to draw with GL_TRIANGLES
-        mesh += (drawCount / 6) * 4;
-    }
-}
-
 void OpenGLRenderer::clearLayerRegions() {
     const size_t quadCount = mLayers.size();
     if (quadCount == 0) return;
@@ -1290,34 +1123,19 @@
             Vertex::set(vertex++, bounds.right, bounds.bottom);
         }
         // We must clear the list of dirty rects before we
-        // call setupDraw() to prevent stencil setup to do
-        // the same thing again
+        // call clearLayerRegions() in renderGlop to prevent
+        // stencil setup from doing the same thing again
         mLayers.clear();
 
-        if (USE_GLOPS) {
-            Glop glop;
-            GlopBuilder(mRenderState, mCaches, &glop)
-                    .setMeshIndexedQuads(&mesh[0], quadCount)
-                    .setFillClear()
-                    .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
-                    .setModelViewOffsetRect(0, 0, Rect(currentSnapshot()->getClipRect()))
-                    .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                    .build();
-            renderGlop(glop, false);
-        } else {
-            SkPaint clearPaint;
-            clearPaint.setXfermodeMode(SkXfermode::kClear_Mode);
-
-            setupDraw(false);
-            setupDrawColor(0.0f, 0.0f, 0.0f, 1.0f);
-            setupDrawBlending(&clearPaint, true);
-            setupDrawProgram();
-            setupDrawPureColorUniforms();
-            setupDrawModelView(kModelViewMode_Translate, false,
-                    0.0f, 0.0f, 0.0f, 0.0f, true);
-
-            issueIndexedQuadDraw(&mesh[0], quadCount);
-        }
+        Glop glop;
+        GlopBuilder(mRenderState, mCaches, &glop)
+                .setMeshIndexedQuads(&mesh[0], quadCount)
+                .setFillClear()
+                .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
+                .setModelViewOffsetRect(0, 0, Rect(currentSnapshot()->getClipRect()))
+                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+                .build();
+        renderGlop(glop, false);
 
         if (scissorChanged) mRenderState.scissor().setEnabled(true);
     } else {
@@ -1498,35 +1316,16 @@
     mRenderState.scissor().set(scissorBox.left, getViewportHeight() - scissorBox.bottom,
             scissorBox.getWidth(), scissorBox.getHeight());
 
-    if (USE_GLOPS) {
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshIndexedQuads(&rectangleVertices[0], rectangleVertices.size() / 4)
-                .setFillBlack()
-                .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
-                .setModelViewOffsetRect(0, 0, scissorBox)
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    const SkPaint* paint = nullptr;
-    setupDraw();
-    setupDrawNoTexture();
-    setupDrawColor(0, 0xff * currentSnapshot()->alpha);
-    setupDrawShader(getShader(paint));
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawBlending(paint);
-    setupDrawProgram();
-    setupDrawDirtyRegionsDisabled();
-    setupDrawModelView(kModelViewMode_Translate, false,
-            0.0f, 0.0f, 0.0f, 0.0f, true);
-    setupDrawColorUniforms(getShader(paint));
-    setupDrawShaderUniforms(getShader(paint));
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-
-    issueIndexedQuadDraw(&rectangleVertices[0], rectangleVertices.size() / 4);
+    Glop glop;
+    Vertex* vertices = &rectangleVertices[0];
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshIndexedQuads(vertices, rectangleVertices.size() / 4)
+            .setFillBlack()
+            .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
+            .setModelViewOffsetRect(0, 0, scissorBox)
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::setStencilFromClip() {
@@ -1666,355 +1465,6 @@
 }
 
 ///////////////////////////////////////////////////////////////////////////////
-// Drawing commands
-///////////////////////////////////////////////////////////////////////////////
-
-void OpenGLRenderer::setupDraw(bool clearLayer) {
-    // TODO: It would be best if we could do this before quickRejectSetupScissor()
-    //       changes the scissor test state
-    if (clearLayer) clearLayerRegions();
-    // Make sure setScissor & setStencil happen at the beginning of
-    // this method
-    if (mState.getDirtyClip()) {
-        if (mRenderState.scissor().isEnabled()) {
-            setScissorFromClip();
-        }
-
-        setStencilFromClip();
-    }
-
-    mDescription.reset();
-
-    mSetShaderColor = false;
-    mColorSet = false;
-    mColor.a = mColor.r = mColor.g = mColor.b = 0.0f;
-    mTextureUnit = 0;
-    mTrackDirtyRegions = true;
-
-    // Enable debug highlight when what we're about to draw is tested against
-    // the stencil buffer and if stencil highlight debugging is on
-    mDescription.hasDebugHighlight = !mCaches.debugOverdraw
-            && mCaches.debugStencilClip == Caches::kStencilShowHighlight
-            && mRenderState.stencil().isTestEnabled();
-}
-
-void OpenGLRenderer::setupDrawWithTexture(bool isAlpha8) {
-    mDescription.hasTexture = true;
-    mDescription.hasAlpha8Texture = isAlpha8;
-}
-
-void OpenGLRenderer::setupDrawWithTextureAndColor(bool isAlpha8) {
-    mDescription.hasTexture = true;
-    mDescription.hasColors = true;
-    mDescription.hasAlpha8Texture = isAlpha8;
-}
-
-void OpenGLRenderer::setupDrawWithExternalTexture() {
-    mDescription.hasExternalTexture = true;
-}
-
-void OpenGLRenderer::setupDrawNoTexture() {
-    mRenderState.meshState().disableTexCoordsVertexArray();
-}
-
-void OpenGLRenderer::setupDrawVertexAlpha(bool useShadowAlphaInterp) {
-    mDescription.hasVertexAlpha = true;
-    mDescription.useShadowAlphaInterp = useShadowAlphaInterp;
-}
-
-void OpenGLRenderer::setupDrawColor(int color, int alpha) {
-    mColor.a = alpha / 255.0f;
-    mColor.r = mColor.a * ((color >> 16) & 0xFF) / 255.0f;
-    mColor.g = mColor.a * ((color >>  8) & 0xFF) / 255.0f;
-    mColor.b = mColor.a * ((color      ) & 0xFF) / 255.0f;
-    mColorSet = true;
-    mSetShaderColor = mDescription.setColorModulate(mColor.a);
-}
-
-void OpenGLRenderer::setupDrawAlpha8Color(int color, int alpha) {
-    mColor.a = alpha / 255.0f;
-    mColor.r = mColor.a * ((color >> 16) & 0xFF) / 255.0f;
-    mColor.g = mColor.a * ((color >>  8) & 0xFF) / 255.0f;
-    mColor.b = mColor.a * ((color      ) & 0xFF) / 255.0f;
-    mColorSet = true;
-    mSetShaderColor = mDescription.setAlpha8ColorModulate(mColor.r, mColor.g, mColor.b, mColor.a);
-}
-
-void OpenGLRenderer::setupDrawTextGamma(const SkPaint* paint) {
-    mCaches.fontRenderer->describe(mDescription, paint);
-}
-
-void OpenGLRenderer::setupDrawColor(float r, float g, float b, float a) {
-    mColor.a = a;
-    mColor.r = r;
-    mColor.g = g;
-    mColor.b = b;
-    mColorSet = true;
-    mSetShaderColor = mDescription.setColorModulate(a);
-}
-
-void OpenGLRenderer::setupDrawShader(const SkShader* shader) {
-    if (shader != nullptr) {
-        SkiaShader::describe(&mCaches, mDescription, mCaches.extensions(), *shader);
-    }
-}
-
-void OpenGLRenderer::setupDrawColorFilter(const SkColorFilter* filter) {
-    if (filter == nullptr) {
-        return;
-    }
-
-    SkXfermode::Mode mode;
-    if (filter->asColorMode(nullptr, &mode)) {
-        mDescription.colorOp = ProgramDescription::kColorBlend;
-        mDescription.colorMode = mode;
-    } else if (filter->asColorMatrix(nullptr)) {
-        mDescription.colorOp = ProgramDescription::kColorMatrix;
-    }
-}
-
-void OpenGLRenderer::accountForClear(SkXfermode::Mode mode) {
-    if (mColorSet && mode == SkXfermode::kClear_Mode) {
-        mColor.a = 1.0f;
-        mColor.r = mColor.g = mColor.b = 0.0f;
-        mSetShaderColor = mDescription.modulate = true;
-    }
-}
-
-void OpenGLRenderer::setupDrawBlending(const Layer* layer, bool swapSrcDst) {
-    SkXfermode::Mode mode = layer->getMode();
-    // When the blending mode is kClear_Mode, we need to use a modulate color
-    // argb=1,0,0,0
-    accountForClear(mode);
-    // TODO: check shader blending, once we have shader drawing support for layers.
-    bool blend = layer->isBlend()
-            || getLayerAlpha(layer) < 1.0f
-            || (mColorSet && mColor.a < 1.0f)
-            || PaintUtils::isBlendedColorFilter(layer->getColorFilter());
-    chooseBlending(blend, mode, mDescription, swapSrcDst);
-}
-
-void OpenGLRenderer::setupDrawBlending(const SkPaint* paint, bool blend, bool swapSrcDst) {
-    SkXfermode::Mode mode = getXfermodeDirect(paint);
-    // When the blending mode is kClear_Mode, we need to use a modulate color
-    // argb=1,0,0,0
-    accountForClear(mode);
-    blend |= (mColorSet && mColor.a < 1.0f)
-            || (getShader(paint) && !getShader(paint)->isOpaque())
-            || PaintUtils::isBlendedColorFilter(getColorFilter(paint));
-    chooseBlending(blend, mode, mDescription, swapSrcDst);
-}
-
-void OpenGLRenderer::setupDrawProgram() {
-    mCaches.setProgram(mDescription);
-    if (mDescription.hasRoundRectClip) {
-        // TODO: avoid doing this repeatedly, stashing state pointer in program
-        const RoundRectClipState* state = writableSnapshot()->roundRectClipState;
-        const Rect& innerRect = state->innerRect;
-        glUniform4f(mCaches.program().getUniform("roundRectInnerRectLTRB"),
-                innerRect.left, innerRect.top,
-                innerRect.right, innerRect.bottom);
-        glUniformMatrix4fv(mCaches.program().getUniform("roundRectInvTransform"),
-                1, GL_FALSE, &state->matrix.data[0]);
-
-        // add half pixel to round out integer rect space to cover pixel centers
-        float roundedOutRadius = state->radius + 0.5f;
-        glUniform1f(mCaches.program().getUniform("roundRectRadius"),
-                roundedOutRadius);
-    }
-}
-
-void OpenGLRenderer::setupDrawDirtyRegionsDisabled() {
-    mTrackDirtyRegions = false;
-}
-
-void OpenGLRenderer::setupDrawModelView(ModelViewMode mode, bool offset,
-        float left, float top, float right, float bottom, bool ignoreTransform) {
-    mModelViewMatrix.loadTranslate(left, top, 0.0f);
-    if (mode == kModelViewMode_TranslateAndScale) {
-        mModelViewMatrix.scale(right - left, bottom - top, 1.0f);
-    }
-
-    bool dirty = right - left > 0.0f && bottom - top > 0.0f;
-    const Matrix4& transformMatrix = ignoreTransform ? Matrix4::identity() : *currentTransform();
-
-    mCaches.program().set(currentSnapshot()->getOrthoMatrix(),
-            mModelViewMatrix, transformMatrix, offset);
-    if (dirty && mTrackDirtyRegions) {
-        if (!ignoreTransform) {
-            dirtyLayer(left, top, right, bottom, *currentTransform());
-        } else {
-            dirtyLayer(left, top, right, bottom);
-        }
-    }
-}
-
-void OpenGLRenderer::setupDrawColorUniforms(bool hasShader) {
-    if ((mColorSet && !hasShader) || (hasShader && mSetShaderColor)) {
-        mCaches.program().setColor(mColor);
-    }
-}
-
-void OpenGLRenderer::setupDrawPureColorUniforms() {
-    if (mSetShaderColor) {
-        mCaches.program().setColor(mColor);
-    }
-}
-
-void OpenGLRenderer::setupDrawShaderUniforms(const SkShader* shader, bool ignoreTransform) {
-    if (shader == nullptr) {
-        return;
-    }
-
-    if (ignoreTransform) {
-        // if ignoreTransform=true was passed to setupDrawModelView, undo currentTransform()
-        // because it was built into modelView / the geometry, and the description needs to
-        // compensate.
-        mat4 modelViewWithoutTransform;
-        modelViewWithoutTransform.loadInverse(*currentTransform());
-        modelViewWithoutTransform.multiply(mModelViewMatrix);
-        mModelViewMatrix.load(modelViewWithoutTransform);
-    }
-
-    SkiaShader::setupProgram(&mCaches, mModelViewMatrix, &mTextureUnit,
-            mCaches.extensions(), *shader);
-}
-
-void OpenGLRenderer::setupDrawColorFilterUniforms(const SkColorFilter* filter) {
-    if (nullptr == filter) {
-        return;
-    }
-
-    SkColor color;
-    SkXfermode::Mode mode;
-    if (filter->asColorMode(&color, &mode)) {
-        const int alpha = SkColorGetA(color);
-        const GLfloat a = alpha / 255.0f;
-        const GLfloat r = a * SkColorGetR(color) / 255.0f;
-        const GLfloat g = a * SkColorGetG(color) / 255.0f;
-        const GLfloat b = a * SkColorGetB(color) / 255.0f;
-        glUniform4f(mCaches.program().getUniform("colorBlend"), r, g, b, a);
-        return;
-    }
-
-    SkScalar srcColorMatrix[20];
-    if (filter->asColorMatrix(srcColorMatrix)) {
-
-        float colorMatrix[16];
-        memcpy(colorMatrix, srcColorMatrix, 4 * sizeof(float));
-        memcpy(&colorMatrix[4], &srcColorMatrix[5], 4 * sizeof(float));
-        memcpy(&colorMatrix[8], &srcColorMatrix[10], 4 * sizeof(float));
-        memcpy(&colorMatrix[12], &srcColorMatrix[15], 4 * sizeof(float));
-
-        // Skia uses the range [0..255] for the addition vector, but we need
-        // the [0..1] range to apply the vector in GLSL
-        float colorVector[4];
-        colorVector[0] = srcColorMatrix[4] / 255.0f;
-        colorVector[1] = srcColorMatrix[9] / 255.0f;
-        colorVector[2] = srcColorMatrix[14] / 255.0f;
-        colorVector[3] = srcColorMatrix[19] / 255.0f;
-
-        glUniformMatrix4fv(mCaches.program().getUniform("colorMatrix"), 1,
-                GL_FALSE, colorMatrix);
-        glUniform4fv(mCaches.program().getUniform("colorMatrixVector"), 1, colorVector);
-        return;
-    }
-
-    // it is an error if we ever get here
-}
-
-void OpenGLRenderer::setupDrawTextGammaUniforms() {
-    mCaches.fontRenderer->setupProgram(mDescription, mCaches.program());
-}
-
-void OpenGLRenderer::setupDrawSimpleMesh() {
-    bool force = mRenderState.meshState().bindMeshBuffer();
-    mRenderState.meshState().bindPositionVertexPointer(force, nullptr);
-    mRenderState.meshState().unbindIndicesBuffer();
-}
-
-void OpenGLRenderer::setupDrawTexture(GLuint texture) {
-    if (texture) mCaches.textureState().bindTexture(texture);
-    mTextureUnit++;
-    mRenderState.meshState().enableTexCoordsVertexArray();
-}
-
-void OpenGLRenderer::setupDrawExternalTexture(GLuint texture) {
-    mCaches.textureState().bindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
-    mTextureUnit++;
-    mRenderState.meshState().enableTexCoordsVertexArray();
-}
-
-void OpenGLRenderer::setupDrawTextureTransform() {
-    mDescription.hasTextureTransform = true;
-}
-
-void OpenGLRenderer::setupDrawTextureTransformUniforms(mat4& transform) {
-    glUniformMatrix4fv(mCaches.program().getUniform("mainTextureTransform"), 1,
-            GL_FALSE, &transform.data[0]);
-}
-
-void OpenGLRenderer::setupDrawMesh(const GLvoid* vertices,
-        const GLvoid* texCoords, GLuint vbo) {
-    bool force = false;
-    if (!vertices || vbo) {
-        force = mRenderState.meshState().bindMeshBuffer(vbo);
-    } else {
-        force = mRenderState.meshState().unbindMeshBuffer();
-    }
-
-    mRenderState.meshState().bindPositionVertexPointer(force, vertices);
-    if (mCaches.program().texCoords >= 0) {
-        mRenderState.meshState().bindTexCoordsVertexPointer(force, texCoords);
-    }
-
-    mRenderState.meshState().unbindIndicesBuffer();
-}
-
-void OpenGLRenderer::setupDrawMesh(const GLvoid* vertices,
-        const GLvoid* texCoords, const GLvoid* colors) {
-    bool force = mRenderState.meshState().unbindMeshBuffer();
-    GLsizei stride = sizeof(ColorTextureVertex);
-
-    mRenderState.meshState().bindPositionVertexPointer(force, vertices, stride);
-    if (mCaches.program().texCoords >= 0) {
-        mRenderState.meshState().bindTexCoordsVertexPointer(force, texCoords, stride);
-    }
-    int slot = mCaches.program().getAttrib("colors");
-    if (slot >= 0) {
-        glEnableVertexAttribArray(slot);
-        glVertexAttribPointer(slot, 4, GL_FLOAT, GL_FALSE, stride, colors);
-    }
-
-    mRenderState.meshState().unbindIndicesBuffer();
-}
-
-void OpenGLRenderer::setupDrawMeshIndices(const GLvoid* vertices,
-        const GLvoid* texCoords, GLuint vbo) {
-    bool force = false;
-    // If vbo is != 0 we want to treat the vertices parameter as an offset inside
-    // a VBO. However, if vertices is set to NULL and vbo == 0 then we want to
-    // use the default VBO found in RenderState
-    if (!vertices || vbo) {
-        force = mRenderState.meshState().bindMeshBuffer(vbo);
-    } else {
-        force = mRenderState.meshState().unbindMeshBuffer();
-    }
-    mRenderState.meshState().bindQuadIndicesBuffer();
-
-    mRenderState.meshState().bindPositionVertexPointer(force, vertices);
-    if (mCaches.program().texCoords >= 0) {
-        mRenderState.meshState().bindTexCoordsVertexPointer(force, texCoords);
-    }
-}
-
-void OpenGLRenderer::setupDrawIndexedVertices(GLvoid* vertices) {
-    bool force = mRenderState.meshState().unbindMeshBuffer();
-    mRenderState.meshState().bindQuadIndicesBuffer();
-    mRenderState.meshState().bindPositionVertexPointer(force, vertices, kVertexStride);
-}
-
-///////////////////////////////////////////////////////////////////////////////
 // Drawing
 ///////////////////////////////////////////////////////////////////////////////
 
@@ -2049,30 +1499,6 @@
     }
 }
 
-void OpenGLRenderer::drawAlphaBitmap(Texture* texture, const SkPaint* paint) {
-    float x = 0;
-    float y = 0;
-
-    texture->setWrap(GL_CLAMP_TO_EDGE, true);
-
-    bool ignoreTransform = false;
-    if (currentTransform()->isPureTranslate()) {
-        x = floorf(currentTransform()->getTranslateX() + 0.5f);
-        y = floorf(currentTransform()->getTranslateY() + 0.5f);
-        ignoreTransform = true;
-
-        texture->setFilter(GL_NEAREST, true);
-    } else {
-        texture->setFilter(PaintUtils::getFilter(paint), true);
-    }
-
-    // No need to check for a UV mapper on the texture object, only ARGB_8888
-    // bitmaps get packed in the atlas
-    drawAlpha8TextureMesh(x, y, x + texture->width, y + texture->height, texture->id,
-            paint, (GLvoid*) nullptr, (GLvoid*) kMeshTextureOffset,
-            GL_TRIANGLE_STRIP, kUnitQuadCount, ignoreTransform);
-}
-
 /**
  * Important note: this method is intended to draw batches of bitmaps and
  * will not set the scissor enable or dirty the current layer, if any.
@@ -2086,45 +1512,22 @@
 
     const AutoTexture autoCleanup(texture);
 
-    if (USE_GLOPS) {
-        // TODO: remove layer dirty in multi-draw callers
-        // TODO: snap doesn't need to touch transform, only texture filter.
-        bool snap = pureTranslate;
-        const float x = floorf(bounds.left + 0.5f);
-        const float y = floorf(bounds.top + 0.5f);
-        int textureFillFlags = static_cast<int>((bitmap->colorType() == kAlpha_8_SkColorType)
-                ? TextureFillFlags::kIsAlphaMaskTexture : TextureFillFlags::kNone);
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedMesh(vertices, bitmapCount * 6)
-                .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
-                .setModelViewOffsetRectOptionalSnap(snap, x, y, Rect(0, 0, bounds.getWidth(), bounds.getHeight()))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    mCaches.textureState().activateTexture(0);
-    texture->setWrap(GL_CLAMP_TO_EDGE, true);
-    texture->setFilter(pureTranslate ? GL_NEAREST : PaintUtils::getFilter(paint), true);
-
+    // TODO: remove layer dirty in multi-draw callers
+    // TODO: snap doesn't need to touch transform, only texture filter.
+    bool snap = pureTranslate;
     const float x = floorf(bounds.left + 0.5f);
     const float y = floorf(bounds.top + 0.5f);
-    if (CC_UNLIKELY(bitmap->colorType() == kAlpha_8_SkColorType)) {
-        drawAlpha8TextureMesh(x, y, x + bounds.getWidth(), y + bounds.getHeight(),
-                texture->id, paint, &vertices[0].x, &vertices[0].u,
-                GL_TRIANGLES, bitmapCount * 6, true,
-                kModelViewMode_Translate, false);
-    } else {
-        drawTextureMesh(x, y, x + bounds.getWidth(), y + bounds.getHeight(),
-                texture->id, paint, texture->blend, &vertices[0].x, &vertices[0].u,
-                GL_TRIANGLES, bitmapCount * 6, false, true, 0,
-                kModelViewMode_Translate, false);
-    }
-
-    mDirty = true;
+    int textureFillFlags = static_cast<int>((bitmap->colorType() == kAlpha_8_SkColorType)
+            ? TextureFillFlags::kIsAlphaMaskTexture : TextureFillFlags::kNone);
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedMesh(vertices, bitmapCount * 6)
+            .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
+            .setModelViewOffsetRectOptionalSnap(snap, x, y, Rect(0, 0, bounds.getWidth(), bounds.getHeight()))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::drawBitmap(const SkBitmap* bitmap, const SkPaint* paint) {
@@ -2137,28 +1540,17 @@
     if (!texture) return;
     const AutoTexture autoCleanup(texture);
 
-    if (USE_GLOPS) {
-        int textureFillFlags = static_cast<int>((bitmap->colorType() == kAlpha_8_SkColorType)
-                ? TextureFillFlags::kIsAlphaMaskTexture : TextureFillFlags::kNone);
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedUnitQuad(texture->uvMapper)
-                .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    if (CC_UNLIKELY(bitmap->colorType() == kAlpha_8_SkColorType)) {
-        drawAlphaBitmap(texture, paint);
-    } else {
-        drawTextureRect(texture, paint);
-    }
-
-    mDirty = true;
+    int textureFillFlags = static_cast<int>((bitmap->colorType() == kAlpha_8_SkColorType)
+            ? TextureFillFlags::kIsAlphaMaskTexture : TextureFillFlags::kNone);
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedUnitQuad(texture->uvMapper)
+            .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::drawBitmapMesh(const SkBitmap* bitmap, int meshWidth, int meshHeight,
@@ -2235,56 +1627,19 @@
     }
     const AutoTexture autoCleanup(texture);
 
-    if (USE_GLOPS) {
-        /*
-         * TODO: handle alpha_8 textures correctly by applying paint color, but *not*
-         * shader in that case to mimic the behavior in SkiaCanvas::drawBitmapMesh.
-         */
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshColoredTexturedMesh(mesh.get(), elementCount)
-                .setFillTexturePaint(*texture, static_cast<int>(TextureFillFlags::kNone), paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewOffsetRect(0, 0, Rect(left, top, right, bottom))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    mCaches.textureState().activateTexture(0);
-    texture->setWrap(GL_CLAMP_TO_EDGE, true);
-    texture->setFilter(PaintUtils::getFilter(paint), true);
-
-    int alpha;
-    SkXfermode::Mode mode;
-    getAlphaAndMode(paint, &alpha, &mode);
-
-
-    dirtyLayer(left, top, right, bottom, *currentTransform());
-
-    float a = alpha / 255.0f;
-    setupDraw();
-    setupDrawWithTextureAndColor();
-    setupDrawColor(a, a, a, a);
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawBlending(paint, true);
-    setupDrawProgram();
-    setupDrawDirtyRegionsDisabled();
-    setupDrawModelView(kModelViewMode_Translate, false, 0, 0, 0, 0);
-    setupDrawTexture(texture->id);
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawMesh(&mesh[0].x, &mesh[0].u, &mesh[0].r);
-
-    glDrawArrays(GL_TRIANGLES, 0, elementCount);
-
-    int slot = mCaches.program().getAttrib("colors");
-    if (slot >= 0) {
-        glDisableVertexAttribArray(slot);
-    }
-
-    mDirty = true;
+    /*
+     * TODO: handle alpha_8 textures correctly by applying paint color, but *not*
+     * shader in that case to mimic the behavior in SkiaCanvas::drawBitmapMesh.
+     */
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshColoredTexturedMesh(mesh.get(), elementCount)
+            .setFillTexturePaint(*texture, static_cast<int>(TextureFillFlags::kNone), paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewOffsetRect(0, 0, Rect(left, top, right, bottom))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::drawBitmap(const SkBitmap* bitmap, Rect src, Rect dst, const SkPaint* paint) {
@@ -2296,80 +1651,22 @@
     if (!texture) return;
     const AutoTexture autoCleanup(texture);
 
-    if (USE_GLOPS) {
-        Rect uv(fmax(0.0f, src.left / texture->width),
-                fmax(0.0f, src.top / texture->height),
-                fmin(1.0f, src.right / texture->width),
-                fmin(1.0f, src.bottom / texture->height));
+    Rect uv(fmax(0.0f, src.left / texture->width),
+            fmax(0.0f, src.top / texture->height),
+            fmin(1.0f, src.right / texture->width),
+            fmin(1.0f, src.bottom / texture->height));
 
-        int textureFillFlags = static_cast<int>((bitmap->colorType() == kAlpha_8_SkColorType)
-                ? TextureFillFlags::kIsAlphaMaskTexture : TextureFillFlags::kNone);
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedUvQuad(texture->uvMapper, uv)
-                .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewMapUnitToRectSnap(dst)
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    mCaches.textureState().activateTexture(0);
-
-    const float width = texture->width;
-    const float height = texture->height;
-
-    float u1 = fmax(0.0f, src.left / width);
-    float v1 = fmax(0.0f, src.top / height);
-    float u2 = fmin(1.0f, src.right / width);
-    float v2 = fmin(1.0f, src.bottom / height);
-
-    getMapper(texture).map(u1, v1, u2, v2);
-
-    mRenderState.meshState().unbindMeshBuffer();
-    resetDrawTextureTexCoords(u1, v1, u2, v2);
-
-    texture->setWrap(GL_CLAMP_TO_EDGE, true);
-
-    float scaleX = (dst.right - dst.left) / (src.right - src.left);
-    float scaleY = (dst.bottom - dst.top) / (src.bottom - src.top);
-
-    bool scaled = scaleX != 1.0f || scaleY != 1.0f;
-    bool ignoreTransform = false;
-
-    if (CC_LIKELY(currentTransform()->isPureTranslate())) {
-        float x = floorf(dst.left + currentTransform()->getTranslateX() + 0.5f);
-        float y = floorf(dst.top + currentTransform()->getTranslateY() + 0.5f);
-
-        dst.right = x + (dst.right - dst.left);
-        dst.bottom = y + (dst.bottom - dst.top);
-
-        dst.left = x;
-        dst.top = y;
-
-        texture->setFilter(scaled ? PaintUtils::getFilter(paint) : GL_NEAREST, true);
-        ignoreTransform = true;
-    } else {
-        texture->setFilter(PaintUtils::getFilter(paint), true);
-    }
-
-    if (CC_UNLIKELY(bitmap->colorType() == kAlpha_8_SkColorType)) {
-        drawAlpha8TextureMesh(dst.left, dst.top, dst.right, dst.bottom,
-                texture->id, paint,
-                &mMeshVertices[0].x, &mMeshVertices[0].u,
-                GL_TRIANGLE_STRIP, kUnitQuadCount, ignoreTransform);
-    } else {
-        drawTextureMesh(dst.left, dst.top, dst.right, dst.bottom,
-                texture->id, paint, texture->blend,
-                &mMeshVertices[0].x, &mMeshVertices[0].u,
-                GL_TRIANGLE_STRIP, kUnitQuadCount, false, ignoreTransform);
-    }
-
-    resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f);
-
-    mDirty = true;
+    int textureFillFlags = static_cast<int>((bitmap->colorType() == kAlpha_8_SkColorType)
+            ? TextureFillFlags::kIsAlphaMaskTexture : TextureFillFlags::kNone);
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedUvQuad(texture->uvMapper, uv)
+            .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewMapUnitToRectSnap(dst)
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::drawPatch(const SkBitmap* bitmap, const Patch* mesh,
@@ -2382,66 +1679,20 @@
     Texture* texture = entry ? entry->texture : mCaches.textureCache.get(bitmap);
     if (!texture) return;
 
-    if (USE_GLOPS) {
-        // 9 patches are built for stretching - always filter
-        int textureFillFlags = static_cast<int>(TextureFillFlags::kForceFilter);
-        if (bitmap->colorType() == kAlpha_8_SkColorType) {
-            textureFillFlags |= TextureFillFlags::kIsAlphaMaskTexture;
-        }
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshPatchQuads(*mesh)
-                .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewOffsetRectSnap(left, top, Rect(0, 0, right - left, bottom - top)) // TODO: get minimal bounds from patch
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
+    // 9 patches are built for stretching - always filter
+    int textureFillFlags = static_cast<int>(TextureFillFlags::kForceFilter);
+    if (bitmap->colorType() == kAlpha_8_SkColorType) {
+        textureFillFlags |= TextureFillFlags::kIsAlphaMaskTexture;
     }
-
-    mCaches.textureState().activateTexture(0);
-    const AutoTexture autoCleanup(texture);
-
-    texture->setWrap(GL_CLAMP_TO_EDGE, true);
-    texture->setFilter(GL_LINEAR, true);
-
-    const bool pureTranslate = currentTransform()->isPureTranslate();
-    // Mark the current layer dirty where we are going to draw the patch
-    if (hasLayer() && mesh->hasEmptyQuads) {
-        const float offsetX = left + currentTransform()->getTranslateX();
-        const float offsetY = top + currentTransform()->getTranslateY();
-        const size_t count = mesh->quads.size();
-        for (size_t i = 0; i < count; i++) {
-            const Rect& bounds = mesh->quads.itemAt(i);
-            if (CC_LIKELY(pureTranslate)) {
-                const float x = floorf(bounds.left + offsetX + 0.5f);
-                const float y = floorf(bounds.top + offsetY + 0.5f);
-                dirtyLayer(x, y, x + bounds.getWidth(), y + bounds.getHeight());
-            } else {
-                dirtyLayer(left + bounds.left, top + bounds.top,
-                        left + bounds.right, top + bounds.bottom, *currentTransform());
-            }
-        }
-    }
-
-    bool ignoreTransform = false;
-    if (CC_LIKELY(pureTranslate)) {
-        const float x = floorf(left + currentTransform()->getTranslateX() + 0.5f);
-        const float y = floorf(top + currentTransform()->getTranslateY() + 0.5f);
-
-        right = x + right - left;
-        bottom = y + bottom - top;
-        left = x;
-        top = y;
-        ignoreTransform = true;
-    }
-    drawIndexedTextureMesh(left, top, right, bottom, texture->id, paint,
-            texture->blend, (GLvoid*) mesh->positionOffset, (GLvoid*) mesh->textureOffset,
-            GL_TRIANGLES, mesh->indexCount, false, ignoreTransform,
-            mCaches.patchCache.getMeshBuffer(), kModelViewMode_Translate, !mesh->hasEmptyQuads);
-
-    mDirty = true;
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshPatchQuads(*mesh)
+            .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewOffsetRectSnap(left, top, Rect(0, 0, right - left, bottom - top)) // TODO: get minimal bounds from patch
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 /**
@@ -2456,33 +1707,21 @@
     if (!texture) return;
     const AutoTexture autoCleanup(texture);
 
-    if (USE_GLOPS) {
-        // TODO: get correct bounds from caller
-        // 9 patches are built for stretching - always filter
-        int textureFillFlags = static_cast<int>(TextureFillFlags::kForceFilter);
-        if (bitmap->colorType() == kAlpha_8_SkColorType) {
-            textureFillFlags |= TextureFillFlags::kIsAlphaMaskTexture;
-        }
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedIndexedQuads(vertices, elementCount)
-                .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
-                .setModelViewOffsetRect(0, 0, Rect(0, 0, 0, 0))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
+    // TODO: get correct bounds from caller
+    // 9 patches are built for stretching - always filter
+    int textureFillFlags = static_cast<int>(TextureFillFlags::kForceFilter);
+    if (bitmap->colorType() == kAlpha_8_SkColorType) {
+        textureFillFlags |= TextureFillFlags::kIsAlphaMaskTexture;
     }
-
-    texture->setWrap(GL_CLAMP_TO_EDGE, true);
-    texture->setFilter(GL_LINEAR, true);
-
-    drawIndexedTextureMesh(0.0f, 0.0f, 1.0f, 1.0f, texture->id, paint,
-            texture->blend, &vertices[0].x, &vertices[0].u,
-            GL_TRIANGLES, elementCount, false, true, 0, kModelViewMode_Translate, false);
-
-    mDirty = true;
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedIndexedQuads(vertices, elementCount)
+            .setFillTexturePaint(*texture, textureFillFlags, paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), Matrix4::identity(), false)
+            .setModelViewOffsetRect(0, 0, Rect(0, 0, 0, 0))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::drawVertexBuffer(float translateX, float translateY,
@@ -2493,72 +1732,17 @@
         return;
     }
 
-    if (USE_GLOPS) {
-        bool fudgeOffset = displayFlags & kVertexBuffer_Offset;
-        bool shadowInterp = displayFlags & kVertexBuffer_ShadowInterp;
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshVertexBuffer(vertexBuffer, shadowInterp)
-                .setFillPaint(*paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), fudgeOffset)
-                .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    const VertexBuffer::MeshFeatureFlags meshFeatureFlags = vertexBuffer.getMeshFeatureFlags();
-    Rect bounds(vertexBuffer.getBounds());
-    bounds.translate(translateX, translateY);
-    dirtyLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, *currentTransform());
-
-    int color = paint->getColor();
-    bool isAA = meshFeatureFlags & VertexBuffer::kAlpha;
-
-    setupDraw();
-    setupDrawNoTexture();
-    if (isAA) setupDrawVertexAlpha((displayFlags & kVertexBuffer_ShadowInterp));
-    setupDrawColor(color, ((color >> 24) & 0xFF) * currentSnapshot()->alpha);
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawShader(getShader(paint));
-    setupDrawBlending(paint, isAA);
-    setupDrawProgram();
-    setupDrawModelView(kModelViewMode_Translate, (displayFlags & kVertexBuffer_Offset),
-            translateX, translateY, 0, 0);
-    setupDrawColorUniforms(getShader(paint));
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawShaderUniforms(getShader(paint));
-
-    const void* vertices = vertexBuffer.getBuffer();
-    mRenderState.meshState().unbindMeshBuffer();
-    mRenderState.meshState().bindPositionVertexPointer(true, vertices,
-            isAA ? kAlphaVertexStride : kVertexStride);
-    mRenderState.meshState().resetTexCoordsVertexPointer();
-
-    int alphaSlot = -1;
-    if (isAA) {
-        void* alphaCoords = ((GLbyte*) vertices) + kVertexAlphaOffset;
-        alphaSlot = mCaches.program().getAttrib("vtxAlpha");
-        // TODO: avoid enable/disable in back to back uses of the alpha attribute
-        glEnableVertexAttribArray(alphaSlot);
-        glVertexAttribPointer(alphaSlot, 1, GL_FLOAT, GL_FALSE, kAlphaVertexStride, alphaCoords);
-    }
-
-    if (meshFeatureFlags & VertexBuffer::kIndices) {
-        mRenderState.meshState().unbindIndicesBuffer();
-        glDrawElements(GL_TRIANGLE_STRIP, vertexBuffer.getIndexCount(),
-                GL_UNSIGNED_SHORT, vertexBuffer.getIndices());
-    } else {
-        mRenderState.meshState().unbindIndicesBuffer();
-        glDrawArrays(GL_TRIANGLE_STRIP, 0, vertexBuffer.getVertexCount());
-    }
-
-    if (isAA) {
-        glDisableVertexAttribArray(alphaSlot);
-    }
-
-    mDirty = true;
+    bool fudgeOffset = displayFlags & kVertexBuffer_Offset;
+    bool shadowInterp = displayFlags & kVertexBuffer_ShadowInterp;
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshVertexBuffer(vertexBuffer, shadowInterp)
+            .setFillPaint(*paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), fudgeOffset)
+            .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 /**
@@ -2813,40 +1997,15 @@
     const float sx = x - texture->left + textShadow.dx;
     const float sy = y - texture->top + textShadow.dy;
 
-    if (USE_GLOPS) {
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedUnitQuad(nullptr)
-                .setFillShadowTexturePaint(*texture, textShadow.color, *paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    const int shadowAlpha = ((textShadow.color >> 24) & 0xFF) * currentSnapshot()->alpha;
-    if (getShader(paint)) {
-        textShadow.color = SK_ColorWHITE;
-    }
-
-    setupDraw();
-    setupDrawWithTexture(true);
-    setupDrawAlpha8Color(textShadow.color, shadowAlpha < 255 ? shadowAlpha : alpha);
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawShader(getShader(paint));
-    setupDrawBlending(paint, true);
-    setupDrawProgram();
-    setupDrawModelView(kModelViewMode_TranslateAndScale, false,
-            sx, sy, sx + texture->width, sy + texture->height);
-    setupDrawTexture(texture->id);
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawShaderUniforms(getShader(paint));
-    setupDrawMesh(nullptr, (GLvoid*) kMeshTextureOffset);
-
-    glDrawArrays(GL_TRIANGLE_STRIP, 0, kUnitQuadCount);
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedUnitQuad(nullptr)
+            .setFillShadowTexturePaint(*texture, textShadow.color, *paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 bool OpenGLRenderer::canSkipText(const SkPaint* paint) const {
@@ -2898,7 +2057,7 @@
     const Rect& clip(pureTranslate ? writableSnapshot()->getClipRect() : writableSnapshot()->getLocalClip());
     Rect bounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
 
-    TextSetupFunctor functor(this, x, y, pureTranslate, alpha, mode, paint);
+    TextDrawFunctor functor(this, x, y, pureTranslate, alpha, mode, paint);
     if (fontRenderer.renderPosText(paint, &clip, text, 0, bytesCount, count, x, y,
             positions, hasLayer() ? &bounds : nullptr, &functor)) {
         dirtyLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, *currentTransform());
@@ -3050,7 +2209,7 @@
     Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
 
     bool status;
-    TextSetupFunctor functor(this, x, y, pureTranslate, alpha, mode, paint);
+    TextDrawFunctor functor(this, x, y, pureTranslate, alpha, mode, paint);
 
     // don't call issuedrawcommand, do it at end of batch
     bool forceFinish = (drawOpMode != DrawOpMode::kDefer);
@@ -3092,7 +2251,7 @@
     int alpha;
     SkXfermode::Mode mode;
     getAlphaAndMode(paint, &alpha, &mode);
-    TextSetupFunctor functor(this, 0.0f, 0.0f, false, alpha, mode, paint);
+    TextDrawFunctor functor(this, 0.0f, 0.0f, false, alpha, mode, paint);
 
     const Rect* clip = &writableSnapshot()->getLocalClip();
     Rect bounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
@@ -3159,56 +2318,15 @@
             DRAW_DOUBLE_STENCIL_IF(!layer->hasDrawnSinceUpdate,
                     composeLayerRect(layer, layer->regionRect));
         } else if (layer->mesh) {
-            if (USE_GLOPS) {
-                Glop glop;
-                GlopBuilder(mRenderState, mCaches, &glop)
-                        .setMeshTexturedIndexedQuads(layer->mesh, layer->meshElementCount)
-                        .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), Blend::ModeOrderSwap::NoSwap)
-                        .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                        .setModelViewOffsetRectSnap(x, y, Rect(0, 0, layer->layer.getWidth(), layer->layer.getHeight()))
-                        .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                        .build();
-                DRAW_DOUBLE_STENCIL_IF(!layer->hasDrawnSinceUpdate, renderGlop(glop));
-            } else {
-                const float a = getLayerAlpha(layer);
-                setupDraw();
-                setupDrawWithTexture();
-                setupDrawColor(a, a, a, a);
-                setupDrawColorFilter(layer->getColorFilter());
-                setupDrawBlending(layer);
-                setupDrawProgram();
-                setupDrawPureColorUniforms();
-                setupDrawColorFilterUniforms(layer->getColorFilter());
-                setupDrawTexture(layer->getTextureId());
-                if (CC_LIKELY(currentTransform()->isPureTranslate())) {
-                    int tx = (int) floorf(x + currentTransform()->getTranslateX() + 0.5f);
-                    int ty = (int) floorf(y + currentTransform()->getTranslateY() + 0.5f);
-
-                    layer->setFilter(GL_NEAREST);
-                    setupDrawModelView(kModelViewMode_Translate, false, tx, ty,
-                            tx + layer->layer.getWidth(), ty + layer->layer.getHeight(), true);
-                } else {
-                    layer->setFilter(GL_LINEAR);
-                    setupDrawModelView(kModelViewMode_Translate, false, x, y,
-                            x + layer->layer.getWidth(), y + layer->layer.getHeight());
-                }
-
-                TextureVertex* mesh = &layer->mesh[0];
-                GLsizei elementsCount = layer->meshElementCount;
-
-                while (elementsCount > 0) {
-                    GLsizei drawCount = MathUtils::min(elementsCount, (GLsizei) kMaxNumberOfQuads * 6);
-
-                    setupDrawMeshIndices(&mesh[0].x, &mesh[0].u);
-                    DRAW_DOUBLE_STENCIL_IF(!layer->hasDrawnSinceUpdate,
-                            glDrawElements(GL_TRIANGLES, drawCount, GL_UNSIGNED_SHORT, nullptr));
-
-                    elementsCount -= drawCount;
-                    // Though there are 4 vertices in a quad, we use 6 indices per
-                    // quad to draw with GL_TRIANGLES
-                    mesh += (drawCount / 6) * 4;
-                }
-            }
+            Glop glop;
+            GlopBuilder(mRenderState, mCaches, &glop)
+                    .setMeshTexturedIndexedQuads(layer->mesh, layer->meshElementCount)
+                    .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), Blend::ModeOrderSwap::NoSwap)
+                    .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+                    .setModelViewOffsetRectSnap(x, y, Rect(0, 0, layer->layer.getWidth(), layer->layer.getHeight()))
+                    .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+                    .build();
+            DRAW_DOUBLE_STENCIL_IF(!layer->hasDrawnSinceUpdate, renderGlop(glop));
 #if DEBUG_LAYERS_AS_REGIONS
             drawRegionRectsDebug(layer->region);
 #endif
@@ -3258,40 +2376,15 @@
         return;
     }
 
-    if (USE_GLOPS) {
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshTexturedUnitQuad(nullptr)
-                .setFillPathTexturePaint(*texture, *paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
-                .setModelViewMapUnitToRect(Rect(x, y, x + texture->width, y + texture->height))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-
-    int alpha;
-    SkXfermode::Mode mode;
-    getAlphaAndMode(paint, &alpha, &mode);
-
-    setupDraw();
-    setupDrawWithTexture(true);
-    setupDrawAlpha8Color(paint->getColor(), alpha);
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawShader(getShader(paint));
-    setupDrawBlending(paint, true);
-    setupDrawProgram();
-    setupDrawModelView(kModelViewMode_TranslateAndScale, false,
-            x, y, x + texture->width, y + texture->height);
-    setupDrawTexture(texture->id);
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawShaderUniforms(getShader(paint));
-    setupDrawMesh(nullptr, (GLvoid*) kMeshTextureOffset);
-
-    glDrawArrays(GL_TRIANGLE_STRIP, 0, kUnitQuadCount);
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshTexturedUnitQuad(nullptr)
+            .setFillPathTexturePaint(*texture, *paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+            .setModelViewMapUnitToRect(Rect(x, y, x + texture->width, y + texture->height))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 // Same values used by Skia
@@ -3419,249 +2512,30 @@
         return;
     }
 
-    if (USE_GLOPS) {
-        const Matrix4& transform = ignoreTransform ? Matrix4::identity() : *currentTransform();
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshIndexedQuads(&mesh[0], count / 4)
-                .setFillPaint(*paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), transform, false)
-                .setModelViewOffsetRect(0, 0, Rect(left, top, right, bottom))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    int color = paint->getColor();
-    // If a shader is set, preserve only the alpha
-    if (getShader(paint)) {
-        color |= 0x00ffffff;
-    }
-
-    setupDraw();
-    setupDrawNoTexture();
-    setupDrawColor(color, ((color >> 24) & 0xFF) * currentSnapshot()->alpha);
-    setupDrawShader(getShader(paint));
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawBlending(paint);
-    setupDrawProgram();
-    setupDrawDirtyRegionsDisabled();
-    setupDrawModelView(kModelViewMode_Translate, false,
-            0.0f, 0.0f, 0.0f, 0.0f, ignoreTransform);
-    setupDrawColorUniforms(getShader(paint));
-    setupDrawShaderUniforms(getShader(paint));
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-
-    if (dirty && hasLayer()) {
-        dirtyLayer(left, top, right, bottom, *currentTransform());
-    }
-
-    issueIndexedQuadDraw(&mesh[0], count / 4);
-
-    mDirty = true;
+    const Matrix4& transform = ignoreTransform ? Matrix4::identity() : *currentTransform();
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshIndexedQuads(&mesh[0], count / 4)
+            .setFillPaint(*paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), transform, false)
+            .setModelViewOffsetRect(0, 0, Rect(left, top, right, bottom))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::drawColorRect(float left, float top, float right, float bottom,
         const SkPaint* paint, bool ignoreTransform) {
-
-    if (USE_GLOPS) {
-        const Matrix4& transform = ignoreTransform ? Matrix4::identity() : *currentTransform();
-        Glop glop;
-        GlopBuilder(mRenderState, mCaches, &glop)
-                .setMeshUnitQuad()
-                .setFillPaint(*paint, currentSnapshot()->alpha)
-                .setTransform(currentSnapshot()->getOrthoMatrix(), transform, false)
-                .setModelViewMapUnitToRect(Rect(left, top, right, bottom))
-                .setRoundRectClipState(currentSnapshot()->roundRectClipState)
-                .build();
-        renderGlop(glop);
-        return;
-    }
-
-    int color = paint->getColor();
-    // If a shader is set, preserve only the alpha
-    if (getShader(paint)) {
-        color |= 0x00ffffff;
-    }
-
-    setupDraw();
-    setupDrawNoTexture();
-    setupDrawColor(color, ((color >> 24) & 0xFF) * currentSnapshot()->alpha);
-    setupDrawShader(getShader(paint));
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawBlending(paint);
-    setupDrawProgram();
-    setupDrawModelView(kModelViewMode_TranslateAndScale, false,
-            left, top, right, bottom, ignoreTransform);
-    setupDrawColorUniforms(getShader(paint));
-    setupDrawShaderUniforms(getShader(paint), ignoreTransform);
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawSimpleMesh();
-
-    glDrawArrays(GL_TRIANGLE_STRIP, 0, kUnitQuadCount);
-}
-
-void OpenGLRenderer::drawTextureRect(Texture* texture, const SkPaint* paint) {
-    texture->setWrap(GL_CLAMP_TO_EDGE, true);
-
-    GLvoid* vertices = (GLvoid*) nullptr;
-    GLvoid* texCoords = (GLvoid*) kMeshTextureOffset;
-
-    if (texture->uvMapper) {
-        vertices = &mMeshVertices[0].x;
-        texCoords = &mMeshVertices[0].u;
-
-        Rect uvs(0.0f, 0.0f, 1.0f, 1.0f);
-        texture->uvMapper->map(uvs);
-
-        resetDrawTextureTexCoords(uvs.left, uvs.top, uvs.right, uvs.bottom);
-    }
-
-    if (CC_LIKELY(currentTransform()->isPureTranslate())) {
-        const float x = floorf(currentTransform()->getTranslateX() + 0.5f);
-        const float y = floorf(currentTransform()->getTranslateY() + 0.5f);
-
-        texture->setFilter(GL_NEAREST, true);
-        drawTextureMesh(x, y, x + texture->width, y + texture->height, texture->id,
-                paint, texture->blend, vertices, texCoords,
-                GL_TRIANGLE_STRIP, kUnitQuadCount, false, true);
-    } else {
-        texture->setFilter(PaintUtils::getFilter(paint), true);
-        drawTextureMesh(0, 0, texture->width, texture->height, texture->id, paint,
-                texture->blend, vertices, texCoords, GL_TRIANGLE_STRIP, kUnitQuadCount);
-    }
-
-    if (texture->uvMapper) {
-        resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f);
-    }
-}
-
-void OpenGLRenderer::drawTextureMesh(float left, float top, float right, float bottom,
-        GLuint texture, const SkPaint* paint, bool blend,
-        GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
-        bool swapSrcDst, bool ignoreTransform, GLuint vbo,
-        ModelViewMode modelViewMode, bool dirty) {
-
-    int a;
-    SkXfermode::Mode mode;
-    getAlphaAndMode(paint, &a, &mode);
-    const float alpha = a / 255.0f;
-
-    setupDraw();
-    setupDrawWithTexture();
-    setupDrawColor(alpha, alpha, alpha, alpha);
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawBlending(paint, blend, swapSrcDst);
-    setupDrawProgram();
-    if (!dirty) setupDrawDirtyRegionsDisabled();
-    setupDrawModelView(modelViewMode, false, left, top, right, bottom, ignoreTransform);
-    setupDrawTexture(texture);
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawMesh(vertices, texCoords, vbo);
-
-    glDrawArrays(drawMode, 0, elementsCount);
-}
-
-void OpenGLRenderer::drawIndexedTextureMesh(float left, float top, float right, float bottom,
-        GLuint texture, const SkPaint* paint, bool blend,
-        GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
-        bool swapSrcDst, bool ignoreTransform, GLuint vbo,
-        ModelViewMode modelViewMode, bool dirty) {
-
-    int a;
-    SkXfermode::Mode mode;
-    getAlphaAndMode(paint, &a, &mode);
-    const float alpha = a / 255.0f;
-
-    setupDraw();
-    setupDrawWithTexture();
-    setupDrawColor(alpha, alpha, alpha, alpha);
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawBlending(paint, blend, swapSrcDst);
-    setupDrawProgram();
-    if (!dirty) setupDrawDirtyRegionsDisabled();
-    setupDrawModelView(modelViewMode, false, left, top, right, bottom, ignoreTransform);
-    setupDrawTexture(texture);
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawMeshIndices(vertices, texCoords, vbo);
-
-    glDrawElements(drawMode, elementsCount, GL_UNSIGNED_SHORT, nullptr);
-}
-
-void OpenGLRenderer::drawAlpha8TextureMesh(float left, float top, float right, float bottom,
-        GLuint texture, const SkPaint* paint,
-        GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
-        bool ignoreTransform, ModelViewMode modelViewMode, bool dirty) {
-
-    int color = paint != nullptr ? paint->getColor() : 0;
-    int alpha;
-    SkXfermode::Mode mode;
-    getAlphaAndMode(paint, &alpha, &mode);
-
-    setupDraw();
-    setupDrawWithTexture(true);
-    if (paint != nullptr) {
-        setupDrawAlpha8Color(color, alpha);
-    }
-    setupDrawColorFilter(getColorFilter(paint));
-    setupDrawShader(getShader(paint));
-    setupDrawBlending(paint, true);
-    setupDrawProgram();
-    if (!dirty) setupDrawDirtyRegionsDisabled();
-    setupDrawModelView(modelViewMode, false, left, top, right, bottom, ignoreTransform);
-    setupDrawTexture(texture);
-    setupDrawPureColorUniforms();
-    setupDrawColorFilterUniforms(getColorFilter(paint));
-    setupDrawShaderUniforms(getShader(paint), ignoreTransform);
-    setupDrawMesh(vertices, texCoords);
-
-    glDrawArrays(drawMode, 0, elementsCount);
-}
-
-void OpenGLRenderer::chooseBlending(bool blend, SkXfermode::Mode mode,
-        ProgramDescription& description, bool swapSrcDst) {
-
-    if (currentSnapshot()->roundRectClipState != nullptr /*&& !mSkipOutlineClip*/) {
-        blend = true;
-        mDescription.hasRoundRectClip = true;
-    }
-    mSkipOutlineClip = true;
-
-    blend = blend || mode != SkXfermode::kSrcOver_Mode;
-
-    if (blend) {
-        // These blend modes are not supported by OpenGL directly and have
-        // to be implemented using shaders. Since the shader will perform
-        // the blending, turn blending off here
-        // If the blend mode cannot be implemented using shaders, fall
-        // back to the default SrcOver blend mode instead
-        if (CC_UNLIKELY(mode > SkXfermode::kScreen_Mode)) {
-            if (CC_UNLIKELY(mCaches.extensions().hasFramebufferFetch())) {
-                description.framebufferMode = mode;
-                description.swapSrcDst = swapSrcDst;
-
-                mRenderState.blend().disable();
-                return;
-            } else {
-                mode = SkXfermode::kSrcOver_Mode;
-            }
-        }
-        mRenderState.blend().enable(mode,
-                swapSrcDst ? Blend::ModeOrderSwap::Swap : Blend::ModeOrderSwap::NoSwap);
-    } else {
-        mRenderState.blend().disable();
-    }
-}
-
-void OpenGLRenderer::resetDrawTextureTexCoords(float u1, float v1, float u2, float v2) {
-    TextureVertex* v = &mMeshVertices[0];
-    TextureVertex::setUV(v++, u1, v1);
-    TextureVertex::setUV(v++, u2, v1);
-    TextureVertex::setUV(v++, u1, v2);
-    TextureVertex::setUV(v++, u2, v2);
+    const Matrix4& transform = ignoreTransform ? Matrix4::identity() : *currentTransform();
+    Glop glop;
+    GlopBuilder(mRenderState, mCaches, &glop)
+            .setMeshUnitQuad()
+            .setFillPaint(*paint, currentSnapshot()->alpha)
+            .setTransform(currentSnapshot()->getOrthoMatrix(), transform, false)
+            .setModelViewMapUnitToRect(Rect(left, top, right, bottom))
+            .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+            .build();
+    renderGlop(glop);
 }
 
 void OpenGLRenderer::getAlphaAndMode(const SkPaint* paint, int* alpha,
diff --git a/libs/hwui/OpenGLRenderer.h b/libs/hwui/OpenGLRenderer.h
index f4acad0..5f8960a 100755
--- a/libs/hwui/OpenGLRenderer.h
+++ b/libs/hwui/OpenGLRenderer.h
@@ -65,7 +65,7 @@
 struct Glop;
 class RenderState;
 class RenderNode;
-class TextSetupFunctor;
+class TextDrawFunctor;
 class VertexBuffer;
 
 struct DrawModifiers {
@@ -727,15 +727,6 @@
     void drawShape(float left, float top, PathTexture* texture, const SkPaint* paint);
 
     /**
-     * Draws the specified texture as an alpha bitmap. Alpha bitmaps obey
-     * different compositing rules.
-     *
-     * @param texture The texture to draw with
-     * @param paint The paint to render with
-     */
-    void drawAlphaBitmap(Texture* texture, const SkPaint* paint);
-
-    /**
      * Renders a strip of polygons with the specified paint, used for tessellated geometry.
      *
      * @param vertexBuffer The VertexBuffer to be drawn
@@ -762,60 +753,6 @@
     void drawConvexPath(const SkPath& path, const SkPaint* paint);
 
     /**
-     * Draws a textured rectangle with the specified texture.
-     *
-     * @param texture The texture to use
-     * @param paint The paint containing the alpha, blending mode, etc.
-     */
-    void drawTextureRect(Texture* texture, const SkPaint* paint);
-
-    /**
-     * Draws a textured mesh with the specified texture. If the indices are omitted,
-     * the mesh is drawn as a simple quad. The mesh pointers become offsets when a
-     * VBO is bound.
-     *
-     * @param left The left coordinate of the rectangle
-     * @param top The top coordinate of the rectangle
-     * @param right The right coordinate of the rectangle
-     * @param bottom The bottom coordinate of the rectangle
-     * @param texture The texture name to map onto the rectangle
-     * @param paint The paint containing the alpha, blending mode, colorFilter, etc.
-     * @param blend True if the texture contains an alpha channel
-     * @param vertices The vertices that define the mesh
-     * @param texCoords The texture coordinates of each vertex
-     * @param elementsCount The number of elements in the mesh, required by indices
-     * @param swapSrcDst Whether or not the src and dst blending operations should be swapped
-     * @param ignoreTransform True if the current transform should be ignored
-     * @param vbo The VBO used to draw the mesh
-     * @param modelViewMode Defines whether the model view matrix should be scaled
-     * @param dirty True if calling this method should dirty the current layer
-     */
-    void drawTextureMesh(float left, float top, float right, float bottom, GLuint texture,
-            const SkPaint* paint, bool blend,
-            GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
-            bool swapSrcDst = false, bool ignoreTransform = false, GLuint vbo = 0,
-            ModelViewMode modelViewMode = kModelViewMode_TranslateAndScale, bool dirty = true);
-
-    void drawIndexedTextureMesh(float left, float top, float right, float bottom, GLuint texture,
-            const SkPaint* paint, bool blend,
-            GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
-            bool swapSrcDst = false, bool ignoreTransform = false, GLuint vbo = 0,
-            ModelViewMode modelViewMode = kModelViewMode_TranslateAndScale, bool dirty = true);
-
-    void drawAlpha8TextureMesh(float left, float top, float right, float bottom,
-            GLuint texture, const SkPaint* paint,
-            GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount,
-            bool ignoreTransform, ModelViewMode modelViewMode = kModelViewMode_TranslateAndScale,
-            bool dirty = true);
-
-    /**
-     * Draws the specified list of vertices as quads using indexed GL_TRIANGLES.
-     * If the number of vertices to draw exceeds the number of indices we have
-     * pre-allocated, this method will generate several glDrawElements() calls.
-     */
-    void issueIndexedQuadDraw(Vertex* mesh, GLsizei quadsCount);
-
-    /**
      * Draws text underline and strike-through if needed.
      *
      * @param text The text to decor
@@ -873,78 +810,6 @@
      */
     bool canSkipText(const SkPaint* paint) const;
 
-    /**
-     * Enable or disable blending as necessary. This function sets the appropriate
-     * blend function based on the specified xfermode.
-     */
-    inline void chooseBlending(bool blend, SkXfermode::Mode mode, ProgramDescription& description,
-            bool swapSrcDst = false);
-
-    /**
-     * Invoked before any drawing operation. This sets required state.
-     */
-    void setupDraw(bool clear = true);
-
-    /**
-     * Various methods to setup OpenGL rendering.
-     */
-    void setupDrawWithTexture(bool isAlpha8 = false);
-    void setupDrawWithTextureAndColor(bool isAlpha8 = false);
-    void setupDrawWithExternalTexture();
-    void setupDrawNoTexture();
-    void setupDrawVertexAlpha(bool useShadowAlphaInterp);
-    void setupDrawColor(int color, int alpha);
-    void setupDrawColor(float r, float g, float b, float a);
-    void setupDrawAlpha8Color(int color, int alpha);
-    void setupDrawTextGamma(const SkPaint* paint);
-    void setupDrawShader(const SkShader* shader);
-    void setupDrawColorFilter(const SkColorFilter* filter);
-    void setupDrawBlending(const Layer* layer, bool swapSrcDst = false);
-    void setupDrawBlending(const SkPaint* paint, bool blend = true, bool swapSrcDst = false);
-    void setupDrawProgram();
-    void setupDrawDirtyRegionsDisabled();
-
-    /**
-     * Setup the current program matrices based upon the nature of the geometry.
-     *
-     * @param mode If kModelViewMode_Translate, the geometry must be translated by the left and top
-     * parameters. If kModelViewMode_TranslateAndScale, the geometry that exists in the (0,0, 1,1)
-     * space must be scaled up and translated to fill the quad provided in (l,t,r,b). These
-     * transformations are stored in the modelView matrix and uploaded to the shader.
-     *
-     * @param offset Set to true if the the matrix should be fudged (translated) slightly to
-     * disambiguate geometry pixel positioning. See Vertex::GeometryFudgeFactor().
-     *
-     * @param ignoreTransform Set to true if l,t,r,b coordinates already in layer space,
-     * currentTransform() will be ignored. (e.g. when drawing clip in layer coordinates to stencil,
-     * or when simple translation has been extracted)
-     */
-    void setupDrawModelView(ModelViewMode mode, bool offset,
-            float left, float top, float right, float bottom, bool ignoreTransform = false);
-    void setupDrawColorUniforms(bool hasShader);
-    void setupDrawPureColorUniforms();
-
-    /**
-     * Setup uniforms for the current shader.
-     *
-     * @param shader SkShader on the current paint.
-     *
-     * @param ignoreTransform Set to true to ignore the transform in shader.
-     */
-    void setupDrawShaderUniforms(const SkShader* shader, bool ignoreTransform = false);
-    void setupDrawColorFilterUniforms(const SkColorFilter* paint);
-    void setupDrawSimpleMesh();
-    void setupDrawTexture(GLuint texture);
-    void setupDrawExternalTexture(GLuint texture);
-    void setupDrawTextureTransform();
-    void setupDrawTextureTransformUniforms(mat4& transform);
-    void setupDrawTextGammaUniforms();
-    void setupDrawMesh(const GLvoid* vertices, const GLvoid* texCoords = nullptr, GLuint vbo = 0);
-    void setupDrawMesh(const GLvoid* vertices, const GLvoid* texCoords, const GLvoid* colors);
-    void setupDrawMeshIndices(const GLvoid* vertices, const GLvoid* texCoords, GLuint vbo = 0);
-    void setupDrawIndexedVertices(GLvoid* vertices);
-    void accountForClear(SkXfermode::Mode mode);
-
     bool updateLayer(Layer* layer, bool inFrame);
     void updateLayers();
     void flushLayers();
@@ -993,22 +858,6 @@
     inline Snapshot* writableSnapshot() { return mState.writableSnapshot(); }
     inline const Snapshot* currentSnapshot() const { return mState.currentSnapshot(); }
 
-    /**
-     * Model-view matrix used to position/size objects
-     *
-     * Stores operation-local modifications to the draw matrix that aren't incorporated into the
-     * currentTransform().
-     *
-     * If generated with kModelViewMode_Translate, mModelViewMatrix will reflect an x/y offset,
-     * e.g. the offset in drawLayer(). If generated with kModelViewMode_TranslateAndScale,
-     * mModelViewMatrix will reflect a translation and scale, e.g. the translation and scale
-     * required to make VBO 0 (a rect of (0,0,1,1)) scaled to match the x,y offset, and width/height
-     * of a bitmap.
-     *
-     * Used as input to SkiaShader transformation.
-     */
-    mat4 mModelViewMatrix;
-
     // State used to define the clipping region
     Rect mTilingClip;
     // Is the target render surface opaque
@@ -1016,9 +865,6 @@
     // Is a frame currently being rendered
     bool mFrameStarted;
 
-    // Used to draw textured quads
-    TextureVertex mMeshVertices[4];
-
     // Default UV mapper
     const UvMapper mUvMapper;
 
@@ -1031,21 +877,6 @@
     // List of layers to update at the beginning of a frame
     Vector< sp<Layer> > mLayerUpdates;
 
-    // The following fields are used to setup drawing
-    // Used to describe the shaders to generate
-    ProgramDescription mDescription;
-    // Color description
-    bool mColorSet;
-    FloatColor mColor;
-    // Indicates that the shader should get a color
-    bool mSetShaderColor;
-    // Current texture unit
-    GLuint mTextureUnit;
-    // Track dirty regions, true by default
-    bool mTrackDirtyRegions;
-    // Indicate whether we are drawing an opaque frame
-    bool mOpaqueFrame;
-
     // See PROPERTY_DISABLE_SCISSOR_OPTIMIZATION in
     // Properties.h
     bool mScissorOptimizationDisabled;
@@ -1070,7 +901,7 @@
     std::vector<std::unique_ptr<SkPath>> mTempPaths;
 
     friend class Layer;
-    friend class TextSetupFunctor;
+    friend class TextDrawFunctor;
     friend class DrawBitmapOp;
     friend class DrawPatchOp;
 
diff --git a/libs/hwui/SkiaShader.cpp b/libs/hwui/SkiaShader.cpp
index 2fcf7f3..ecf8b6b 100644
--- a/libs/hwui/SkiaShader.cpp
+++ b/libs/hwui/SkiaShader.cpp
@@ -47,15 +47,6 @@
     return !(n & (n - 1));
 }
 
-static inline void bindUniformColor(int slot, uint32_t color) {
-    const float a = ((color >> 24) & 0xff) / 255.0f;
-    glUniform4f(slot,
-            a * ((color >> 16) & 0xff) / 255.0f,
-            a * ((color >>  8) & 0xff) / 255.0f,
-            a * ((color      ) & 0xff) / 255.0f,
-            a);
-}
-
 static inline void bindUniformColor(int slot, FloatColor color) {
     glUniform4fv(slot, 1, reinterpret_cast<const float*>(&color));
 }
@@ -83,229 +74,11 @@
     screenSpace.multiply(modelViewMatrix);
 }
 
-// Returns true if one is a bitmap and the other is a gradient
-static bool bitmapAndGradient(SkiaShaderType type1, SkiaShaderType type2) {
-    return (type1 == kBitmap_SkiaShaderType && type2 == kGradient_SkiaShaderType)
-            || (type2 == kBitmap_SkiaShaderType && type1 == kGradient_SkiaShaderType);
-}
-
-SkiaShaderType SkiaShader::getType(const SkShader& shader) {
-    // First check for a gradient shader.
-    switch (shader.asAGradient(nullptr)) {
-        case SkShader::kNone_GradientType:
-            // Not a gradient shader. Fall through to check for other types.
-            break;
-        case SkShader::kLinear_GradientType:
-        case SkShader::kRadial_GradientType:
-        case SkShader::kSweep_GradientType:
-            return kGradient_SkiaShaderType;
-        default:
-            // This is a Skia gradient that has no SkiaShader equivalent. Return None to skip.
-            return kNone_SkiaShaderType;
-    }
-
-    // The shader is not a gradient. Check for a bitmap shader.
-    if (shader.asABitmap(nullptr, nullptr, nullptr) == SkShader::kDefault_BitmapType) {
-        return kBitmap_SkiaShaderType;
-    }
-
-    // Check for a ComposeShader.
-    SkShader::ComposeRec rec;
-    if (shader.asACompose(&rec)) {
-        const SkiaShaderType shaderAType = getType(*rec.fShaderA);
-        const SkiaShaderType shaderBType = getType(*rec.fShaderB);
-
-        // Compose is only supported if one is a bitmap and the other is a
-        // gradient. Otherwise, return None to skip.
-        if (!bitmapAndGradient(shaderAType, shaderBType)) {
-            return kNone_SkiaShaderType;
-        }
-        return kCompose_SkiaShaderType;
-    }
-
-    if (shader.asACustomShader(nullptr)) {
-        return kLayer_SkiaShaderType;
-    }
-
-    return kNone_SkiaShaderType;
-}
-
-typedef void (*describeProc)(Caches* caches, ProgramDescription& description,
-        const Extensions& extensions, const SkShader& shader);
-
-describeProc gDescribeProc[] = {
-    InvalidSkiaShader::describe,
-    SkiaBitmapShader::describe,
-    SkiaGradientShader::describe,
-    SkiaComposeShader::describe,
-    SkiaLayerShader::describe,
-};
-
-typedef void (*setupProgramProc)(Caches* caches, const mat4& modelViewMatrix,
-        GLuint* textureUnit, const Extensions& extensions, const SkShader& shader);
-
-setupProgramProc gSetupProgramProc[] = {
-    InvalidSkiaShader::setupProgram,
-    SkiaBitmapShader::setupProgram,
-    SkiaGradientShader::setupProgram,
-    SkiaComposeShader::setupProgram,
-    SkiaLayerShader::setupProgram,
-};
-
-void SkiaShader::describe(Caches* caches, ProgramDescription& description,
-        const Extensions& extensions, const SkShader& shader) {
-    gDescribeProc[getType(shader)](caches, description, extensions, shader);
-}
-
-void SkiaShader::setupProgram(Caches* caches, const mat4& modelViewMatrix,
-        GLuint* textureUnit, const Extensions& extensions, const SkShader& shader) {
-
-    gSetupProgramProc[getType(shader)](caches, modelViewMatrix, textureUnit, extensions, shader);
-}
-
 ///////////////////////////////////////////////////////////////////////////////
-// Layer shader
+// gradient shader matrix helpers
 ///////////////////////////////////////////////////////////////////////////////
 
-void SkiaLayerShader::describe(Caches*, ProgramDescription& description,
-        const Extensions&, const SkShader& shader) {
-    description.hasBitmap = true;
-}
-
-void SkiaLayerShader::setupProgram(Caches* caches, const mat4& modelViewMatrix,
-        GLuint* textureUnit, const Extensions&, const SkShader& shader) {
-    Layer* layer;
-    if (!shader.asACustomShader(reinterpret_cast<void**>(&layer))) {
-        LOG_ALWAYS_FATAL("SkiaLayerShader::setupProgram called on the wrong type of shader!");
-    }
-
-    GLuint textureSlot = (*textureUnit)++;
-    caches->textureState().activateTexture(textureSlot);
-
-    const float width = layer->getWidth();
-    const float height = layer->getHeight();
-
-    mat4 textureTransform;
-    computeScreenSpaceMatrix(textureTransform, SkMatrix::I(), shader.getLocalMatrix(),
-            modelViewMatrix);
-
-
-    // Uniforms
-    layer->bindTexture();
-    layer->setWrap(GL_CLAMP_TO_EDGE);
-    layer->setFilter(GL_LINEAR);
-
-    Program& program = caches->program();
-    glUniform1i(program.getUniform("bitmapSampler"), textureSlot);
-    glUniformMatrix4fv(program.getUniform("textureTransform"), 1,
-            GL_FALSE, &textureTransform.data[0]);
-    glUniform2f(program.getUniform("textureDimension"), 1.0f / width, 1.0f / height);
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Bitmap shader
-///////////////////////////////////////////////////////////////////////////////
-
-struct BitmapShaderInfo {
-    float width;
-    float height;
-    GLenum wrapS;
-    GLenum wrapT;
-    Texture* texture;
-};
-
-static bool bitmapShaderHelper(Caches* caches, ProgramDescription* description,
-        BitmapShaderInfo* shaderInfo,
-        const Extensions& extensions,
-        const SkBitmap& bitmap, SkShader::TileMode tileModes[2]) {
-    Texture* texture = caches->textureCache.get(&bitmap);
-    if (!texture) return false;
-
-    const float width = texture->width;
-    const float height = texture->height;
-    GLenum wrapS, wrapT;
-
-    if (description) {
-        description->hasBitmap = true;
-    }
-    // The driver does not support non-power of two mirrored/repeated
-    // textures, so do it ourselves
-    if (!extensions.hasNPot() && (!isPowerOfTwo(width) || !isPowerOfTwo(height)) &&
-            (tileModes[0] != SkShader::kClamp_TileMode ||
-             tileModes[1] != SkShader::kClamp_TileMode)) {
-        if (description) {
-            description->isBitmapNpot = true;
-            description->bitmapWrapS = gTileModes[tileModes[0]];
-            description->bitmapWrapT = gTileModes[tileModes[1]];
-        }
-        wrapS = GL_CLAMP_TO_EDGE;
-        wrapT = GL_CLAMP_TO_EDGE;
-    } else {
-        wrapS = gTileModes[tileModes[0]];
-        wrapT = gTileModes[tileModes[1]];
-    }
-
-    if (shaderInfo) {
-        shaderInfo->width = width;
-        shaderInfo->height = height;
-        shaderInfo->wrapS = wrapS;
-        shaderInfo->wrapT = wrapT;
-        shaderInfo->texture = texture;
-    }
-    return true;
-}
-
-void SkiaBitmapShader::describe(Caches* caches, ProgramDescription& description,
-        const Extensions& extensions, const SkShader& shader) {
-    SkBitmap bitmap;
-    SkShader::TileMode xy[2];
-    if (shader.asABitmap(&bitmap, nullptr, xy) != SkShader::kDefault_BitmapType) {
-        LOG_ALWAYS_FATAL("SkiaBitmapShader::describe called with a different kind of shader!");
-    }
-    bitmapShaderHelper(caches, &description, nullptr, extensions, bitmap, xy);
-}
-
-void SkiaBitmapShader::setupProgram(Caches* caches, const mat4& modelViewMatrix,
-        GLuint* textureUnit, const Extensions& extensions, const SkShader& shader) {
-    SkBitmap bitmap;
-    SkShader::TileMode xy[2];
-    if (shader.asABitmap(&bitmap, nullptr, xy) != SkShader::kDefault_BitmapType) {
-        LOG_ALWAYS_FATAL("SkiaBitmapShader::setupProgram called with a different kind of shader!");
-    }
-
-    GLuint textureSlot = (*textureUnit)++;
-    caches->textureState().activateTexture(textureSlot);
-
-    BitmapShaderInfo shaderInfo;
-    if (!bitmapShaderHelper(caches, nullptr, &shaderInfo, extensions, bitmap, xy)) {
-        return;
-    }
-
-    Program& program = caches->program();
-    Texture* texture = shaderInfo.texture;
-
-    const AutoTexture autoCleanup(texture);
-
-    mat4 textureTransform;
-    computeScreenSpaceMatrix(textureTransform, SkMatrix::I(), shader.getLocalMatrix(),
-            modelViewMatrix);
-
-    // Uniforms
-    bindTexture(caches, texture, shaderInfo.wrapS, shaderInfo.wrapT);
-    texture->setFilter(GL_LINEAR);
-
-    glUniform1i(program.getUniform("bitmapSampler"), textureSlot);
-    glUniformMatrix4fv(program.getUniform("textureTransform"), 1,
-            GL_FALSE, &textureTransform.data[0]);
-    glUniform2f(program.getUniform("textureDimension"), 1.0f / shaderInfo.width,
-            1.0f / shaderInfo.height);
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Linear gradient shader
-///////////////////////////////////////////////////////////////////////////////
-
-static void toUnitMatrix(const SkPoint pts[2], SkMatrix* matrix) {
+static void toLinearUnitMatrix(const SkPoint pts[2], SkMatrix* matrix) {
     SkVector vec = pts[1] - pts[0];
     const float mag = vec.length();
     const float inv = mag ? 1.0f / mag : 0;
@@ -316,10 +89,6 @@
     matrix->postScale(inv, inv);
 }
 
-///////////////////////////////////////////////////////////////////////////////
-// Circular gradient shader
-///////////////////////////////////////////////////////////////////////////////
-
 static void toCircularUnitMatrix(const float x, const float y, const float radius,
         SkMatrix* matrix) {
     const float inv = 1.0f / radius;
@@ -327,10 +96,6 @@
     matrix->postScale(inv, inv);
 }
 
-///////////////////////////////////////////////////////////////////////////////
-// Sweep gradient shader
-///////////////////////////////////////////////////////////////////////////////
-
 static void toSweepUnitMatrix(const float x, const float y, SkMatrix* matrix) {
     matrix->setTranslate(-x, -y);
 }
@@ -343,137 +108,6 @@
     return gradInfo.fColorCount == 2 && gradInfo.fTileMode == SkShader::kClamp_TileMode;
 }
 
-void SkiaGradientShader::describe(Caches*, ProgramDescription& description,
-        const Extensions& extensions, const SkShader& shader) {
-    SkShader::GradientInfo gradInfo;
-    gradInfo.fColorCount = 0;
-    gradInfo.fColors = nullptr;
-    gradInfo.fColorOffsets = nullptr;
-
-    switch (shader.asAGradient(&gradInfo)) {
-        case SkShader::kLinear_GradientType:
-            description.gradientType = ProgramDescription::kGradientLinear;
-            break;
-        case SkShader::kRadial_GradientType:
-            description.gradientType = ProgramDescription::kGradientCircular;
-            break;
-        case SkShader::kSweep_GradientType:
-            description.gradientType = ProgramDescription::kGradientSweep;
-            break;
-        default:
-            // Do nothing. This shader is unsupported.
-            return;
-    }
-    description.hasGradient = true;
-    description.isSimpleGradient = isSimpleGradient(gradInfo);
-}
-
-void SkiaGradientShader::setupProgram(Caches* caches, const mat4& modelViewMatrix,
-        GLuint* textureUnit, const Extensions&, const SkShader& shader) {
-    // SkShader::GradientInfo.fColorCount is an in/out parameter. As input, it tells asAGradient
-    // how much space has been allocated for fColors and fColorOffsets.  10 was chosen
-    // arbitrarily, but should be >= 2.
-    // As output, it tells the number of actual colors/offsets in the gradient.
-    const int COLOR_COUNT = 10;
-    SkAutoSTMalloc<COLOR_COUNT, SkColor> colorStorage(COLOR_COUNT);
-    SkAutoSTMalloc<COLOR_COUNT, SkScalar> positionStorage(COLOR_COUNT);
-
-    SkShader::GradientInfo gradInfo;
-    gradInfo.fColorCount = COLOR_COUNT;
-    gradInfo.fColors = colorStorage.get();
-    gradInfo.fColorOffsets = positionStorage.get();
-
-    SkShader::GradientType gradType = shader.asAGradient(&gradInfo);
-
-    Program& program = caches->program();
-    if (CC_UNLIKELY(!isSimpleGradient(gradInfo))) {
-        if (gradInfo.fColorCount > COLOR_COUNT) {
-            // There was not enough room in our arrays for all the colors and offsets. Try again,
-            // now that we know the true number of colors.
-            gradInfo.fColors = colorStorage.reset(gradInfo.fColorCount);
-            gradInfo.fColorOffsets = positionStorage.reset(gradInfo.fColorCount);
-
-            shader.asAGradient(&gradInfo);
-        }
-        GLuint textureSlot = (*textureUnit)++;
-        caches->textureState().activateTexture(textureSlot);
-
-#ifndef SK_SCALAR_IS_FLOAT
-    #error Need to convert gradInfo.fColorOffsets to float!
-#endif
-        Texture* texture = caches->gradientCache.get(gradInfo.fColors, gradInfo.fColorOffsets,
-                gradInfo.fColorCount);
-
-        // Uniforms
-        bindTexture(caches, texture, gTileModes[gradInfo.fTileMode], gTileModes[gradInfo.fTileMode]);
-        glUniform1i(program.getUniform("gradientSampler"), textureSlot);
-    } else {
-        bindUniformColor(program.getUniform("startColor"), gradInfo.fColors[0]);
-        bindUniformColor(program.getUniform("endColor"), gradInfo.fColors[1]);
-    }
-
-    caches->dither.setupProgram(program, textureUnit);
-
-    SkMatrix unitMatrix;
-    switch (gradType) {
-        case SkShader::kLinear_GradientType:
-            toUnitMatrix(gradInfo.fPoint, &unitMatrix);
-            break;
-        case SkShader::kRadial_GradientType:
-            toCircularUnitMatrix(gradInfo.fPoint[0].fX, gradInfo.fPoint[0].fY,
-                    gradInfo.fRadius[0], &unitMatrix);
-            break;
-        case SkShader::kSweep_GradientType:
-            toSweepUnitMatrix(gradInfo.fPoint[0].fX, gradInfo.fPoint[0].fY, &unitMatrix);
-            break;
-        default:
-            LOG_ALWAYS_FATAL("Invalid SkShader gradient type %d", gradType);
-    }
-
-    mat4 screenSpace;
-    computeScreenSpaceMatrix(screenSpace, unitMatrix, shader.getLocalMatrix(), modelViewMatrix);
-    glUniformMatrix4fv(program.getUniform("screenSpace"), 1, GL_FALSE, &screenSpace.data[0]);
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Compose shader
-///////////////////////////////////////////////////////////////////////////////
-
-void SkiaComposeShader::describe(Caches* caches, ProgramDescription& description,
-        const Extensions& extensions, const SkShader& shader) {
-    SkShader::ComposeRec rec;
-    if (!shader.asACompose(&rec)) {
-        LOG_ALWAYS_FATAL("SkiaComposeShader::describe called on the wrong shader type!");
-    }
-    SkiaShader::describe(caches, description, extensions, *rec.fShaderA);
-    SkiaShader::describe(caches, description, extensions, *rec.fShaderB);
-    if (SkiaShader::getType(*rec.fShaderA) == kBitmap_SkiaShaderType) {
-        description.isBitmapFirst = true;
-    }
-    if (!SkXfermode::AsMode(rec.fMode, &description.shadersMode)) {
-        // TODO: Support other modes.
-        description.shadersMode = SkXfermode::kSrcOver_Mode;
-    }
-}
-
-void SkiaComposeShader::setupProgram(Caches* caches, const mat4& modelViewMatrix,
-        GLuint* textureUnit, const Extensions& extensions, const SkShader& shader) {
-    SkShader::ComposeRec rec;
-    if (!shader.asACompose(&rec)) {
-        LOG_ALWAYS_FATAL("SkiaComposeShader::setupProgram called on the wrong shader type!");
-    }
-
-    // Apply this compose shader's local transform and pass it down to
-    // the child shaders. They will in turn apply their local transform
-    // to this matrix.
-    mat4 transform;
-    computeScreenSpaceMatrix(transform, SkMatrix::I(), shader.getLocalMatrix(),
-            modelViewMatrix);
-
-    SkiaShader::setupProgram(caches, transform, textureUnit, extensions, *rec.fShaderA);
-    SkiaShader::setupProgram(caches, transform, textureUnit, extensions, *rec.fShaderB);
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Store / apply
 ///////////////////////////////////////////////////////////////////////////////
@@ -491,7 +125,7 @@
         case SkShader::kLinear_GradientType:
             description->gradientType = ProgramDescription::kGradientLinear;
 
-            toUnitMatrix(gradInfo.fPoint, &unitMatrix);
+            toLinearUnitMatrix(gradInfo.fPoint, &unitMatrix);
             break;
         case SkShader::kRadial_GradientType:
             description->gradientType = ProgramDescription::kGradientCircular;
diff --git a/libs/hwui/SkiaShader.h b/libs/hwui/SkiaShader.h
index 2962441c..5b8aa86 100644
--- a/libs/hwui/SkiaShader.h
+++ b/libs/hwui/SkiaShader.h
@@ -87,77 +87,12 @@
 
 class SkiaShader {
 public:
-    static SkiaShaderType getType(const SkShader& shader);
-    static void describe(Caches* caches, ProgramDescription& description,
-            const Extensions& extensions, const SkShader& shader);
-    static void setupProgram(Caches* caches, const mat4& modelViewMatrix,
-            GLuint* textureUnit, const Extensions& extensions, const SkShader& shader);
-
-    // new SkiaShader interaction model - store into ShaderData, and apply to Caches/Program/GL
     static void store(Caches& caches, const SkShader* shader, const Matrix4& modelViewMatrix,
             GLuint* textureUnit, ProgramDescription* description,
             SkiaShaderData* outData);
     static void apply(Caches& caches, const SkiaShaderData& data);
 };
 
-class InvalidSkiaShader {
-public:
-    static void describe(Caches* caches, ProgramDescription& description,
-            const Extensions& extensions, const SkShader& shader) {
-        // This shader is unsupported. Skip it.
-    }
-    static void setupProgram(Caches* caches, const mat4& modelViewMatrix,
-            GLuint* textureUnit, const Extensions& extensions, const SkShader& shader) {
-        // This shader is unsupported. Skip it.
-    }
-
-};
-/**
- * A shader that draws a layer.
- */
-class SkiaLayerShader {
-public:
-    static void describe(Caches* caches, ProgramDescription& description,
-            const Extensions& extensions, const SkShader& shader);
-    static void setupProgram(Caches* caches, const mat4& modelViewMatrix,
-            GLuint* textureUnit, const Extensions& extensions, const SkShader& shader);
-}; // class SkiaLayerShader
-
-/**
- * A shader that draws a bitmap.
- */
-class SkiaBitmapShader {
-public:
-    static void describe(Caches* caches, ProgramDescription& description,
-            const Extensions& extensions, const SkShader& shader);
-    static void setupProgram(Caches* caches, const mat4& modelViewMatrix,
-            GLuint* textureUnit, const Extensions& extensions, const SkShader& shader);
-
-
-}; // class SkiaBitmapShader
-
-/**
- * A shader that draws one of three types of gradient, depending on shader param.
- */
-class SkiaGradientShader {
-public:
-    static void describe(Caches* caches, ProgramDescription& description,
-            const Extensions& extensions, const SkShader& shader);
-    static void setupProgram(Caches* caches, const mat4& modelViewMatrix,
-            GLuint* textureUnit, const Extensions& extensions, const SkShader& shader);
-};
-
-/**
- * A shader that draws two shaders, composited with an xfermode.
- */
-class SkiaComposeShader {
-public:
-    static void describe(Caches* caches, ProgramDescription& description,
-            const Extensions& extensions, const SkShader& shader);
-    static void setupProgram(Caches* caches, const mat4& modelViewMatrix,
-            GLuint* textureUnit, const Extensions& extensions, const SkShader& shader);
-}; // class SkiaComposeShader
-
 }; // namespace uirenderer
 }; // namespace android
 
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 93e2cbe..98bfaff 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -21,7 +21,10 @@
 import java.lang.ref.WeakReference;
 import java.nio.ByteBuffer;
 import java.nio.NioUtils;
+
 import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.app.ActivityThread;
 import android.app.AppOpsManager;
 import android.content.Context;
@@ -113,6 +116,14 @@
      */
     public static final int MODE_STREAM = 1;
 
+    /** @hide */
+    @IntDef({
+        MODE_STATIC,
+        MODE_STREAM
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TransferMode {}
+
     /**
      * State of an AudioTrack that was not successfully initialized upon creation.
      */
@@ -457,6 +468,179 @@
         }
     }
 
+    /**
+     * Builder class for {@link AudioTrack} objects.
+     * Use this class to configure and create an <code>AudioTrack</code> instance. By setting audio
+     * attributes and audio format parameters, you indicate which of those vary from the default
+     * behavior on the device.
+     * <p> Here is an example where <code>Builder</code> is used to specify all {@link AudioFormat}
+     * parameters, to be used by a new <code>AudioTrack</code> instance:
+     *
+     * <pre class="prettyprint">
+     * AudioTrack player = new AudioTrack.Builder()
+     *         .setAudioAttributes(new AudioAttributes.Builder()
+     *                  .setUsage(AudioAttributes.USAGE_ALARM)
+     *                  .setContentType(CONTENT_TYPE_MUSIC)
+     *                  .build())
+     *         .setAudioFormat(new AudioFormat.Builder()
+     *                 .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+     *                 .setSampleRate(441000)
+     *                 .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+     *                 .build())
+     *         .setBufferSize(minBuffSize)
+     *         .build();
+     * </pre>
+     * <p>
+     * If the audio attributes are not set with {@link #setAudioAttributes(AudioAttributes)},
+     * attributes comprising {@link AudioAttributes#USAGE_MEDIA} will be used.
+     * <br>If the audio format is not specified or is incomplete, its sample rate will be the
+     * default output sample rate of the device (see
+     * {@link AudioManager#PROPERTY_OUTPUT_SAMPLE_RATE}), its channel configuration will be
+     * {@link AudioFormat#CHANNEL_OUT_STEREO} and the encoding will be
+     * {@link AudioFormat#ENCODING_PCM_16BIT}.
+     * <br>If the transfer mode is not specified with {@link #setTransferMode(int)},
+     * {@link AudioTrack#MODE_STREAM} will be used.
+     * <br>If the session ID is not specified with {@link #setSessionId(int)}, a new one will
+     * be generated.
+     */
+    public static class Builder {
+        private AudioAttributes mAttributes;
+        private AudioFormat mFormat;
+        private int mBufferSizeInBytes;
+        private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
+        private int mMode = MODE_STREAM;
+
+        /**
+         * Constructs a new Builder with the default values as described above.
+         */
+        public Builder() {
+        }
+
+        /**
+         * Sets the {@link AudioAttributes}.
+         * @param attributes a non-null {@link AudioAttributes} instance that describes the audio
+         *     data to be played.
+         * @return the same Builder instance.
+         * @throws IllegalArgumentException
+         */
+        public @NonNull Builder setAudioAttributes(@NonNull AudioAttributes attributes)
+                throws IllegalArgumentException {
+            if (attributes == null) {
+                throw new IllegalArgumentException("Illegal null AudioAttributes argument");
+            }
+            // keep reference, we only copy the data when building
+            mAttributes = attributes;
+            return this;
+        }
+
+        /**
+         * Sets the format of the audio data to be played by the {@link AudioTrack}.
+         * See {@link AudioFormat.Builder} for configuring the audio format parameters such
+         * as encoding, channel mask and sample rate.
+         * @param format a non-null {@link AudioFormat} instance.
+         * @return the same Builder instance.
+         * @throws IllegalArgumentException
+         */
+        public @NonNull Builder setAudioFormat(@NonNull AudioFormat format)
+                throws IllegalArgumentException {
+            if (format == null) {
+                throw new IllegalArgumentException("Illegal null AudioFormat argument");
+            }
+            // keep reference, we only copy the data when building
+            mFormat = format;
+            return this;
+        }
+
+        /**
+         * Sets the total size (in bytes) of the buffer where audio data is read from for playback.
+         * If using the {@link AudioTrack} in streaming mode
+         * (see {@link AudioTrack#MODE_STREAM}, you can write data into this buffer in smaller
+         * chunks than this size. See {@link #getMinBufferSize(int, int, int)} to determine
+         * the minimum required buffer size for the successful creation of an AudioTrack instance
+         * in streaming mode. Using values smaller than <code>getMinBufferSize()</code> will result
+         * in an exception when trying to build the <code>AudioTrack</code>.
+         * <br>If using the <code>AudioTrack</code> in static mode (see
+         * {@link AudioTrack#MODE_STATIC}), this is the maximum size of the sound that will be
+         * played by this instance.
+         * @param bufferSizeInBytes
+         * @return the same Builder instance.
+         * @throws IllegalArgumentException
+         */
+        public @NonNull Builder setBufferSizeInBytes(int bufferSizeInBytes)
+                throws IllegalArgumentException {
+            if (bufferSizeInBytes <= 0) {
+                throw new IllegalArgumentException("Invalid buffer size " + bufferSizeInBytes);
+            }
+            mBufferSizeInBytes = bufferSizeInBytes;
+            return this;
+        }
+
+        /**
+         * Sets the mode under which buffers of audio data are transferred from the
+         * {@link AudioTrack} to the framework.
+         * @param mode one of {@link AudioTrack#MODE_STREAM}, {@link AudioTrack#MODE_STATIC}.
+         * @return the same Builder instance.
+         * @throws IllegalArgumentException
+         */
+        public @NonNull Builder setTransferMode(@TransferMode int mode)
+                throws IllegalArgumentException {
+            switch(mode) {
+                case MODE_STREAM:
+                case MODE_STATIC:
+                    mMode = mode;
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid transfer mode " + mode);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the session ID the {@link AudioTrack} will be attached to.
+         * @param sessionId a strictly positive ID number retrieved from another
+         *     <code>AudioTrack</code> via {@link AudioTrack#getAudioSessionId()} or allocated by
+         *     {@link AudioManager} via {@link AudioManager#generateAudioSessionId()}, or
+         *     {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
+         * @return the same Builder instance.
+         * @throws IllegalArgumentException
+         */
+        public @NonNull Builder setSessionId(int sessionId)
+                throws IllegalArgumentException {
+            if ((sessionId != AudioManager.AUDIO_SESSION_ID_GENERATE) && (sessionId < 1)) {
+                throw new IllegalArgumentException("Invalid audio session ID " + sessionId);
+            }
+            mSessionId = sessionId;
+            return this;
+        }
+
+        /**
+         * Builds an {@link AudioTrack} instance initialized with all the parameters set
+         * on this <code>Builder</code>.
+         * @return a new {@link AudioTrack} instance.
+         * @throws UnsupportedOperationException if the parameters set on the <code>Builder</code>
+         *     were incompatible, or if they are not supported by the device.
+         */
+        public @NonNull AudioTrack build() throws UnsupportedOperationException {
+            if (mAttributes == null) {
+                mAttributes = new AudioAttributes.Builder()
+                        .setUsage(AudioAttributes.USAGE_MEDIA)
+                        .build();
+            }
+            if (mFormat == null) {
+                mFormat = new AudioFormat.Builder()
+                        .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+                        .setSampleRate(AudioSystem.getPrimaryOutputSamplingRate())
+                        .setEncoding(AudioFormat.ENCODING_DEFAULT)
+                        .build();
+            }
+            try {
+                return new AudioTrack(mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId);
+            } catch (IllegalArgumentException e) {
+                throw new UnsupportedOperationException(e.getMessage());
+            }
+        }
+    }
+
     // mask of all the channels supported by this implementation
     private static final int SUPPORTED_OUT_CHANNELS =
             AudioFormat.CHANNEL_OUT_FRONT_LEFT |
@@ -1749,5 +1933,4 @@
     private static void loge(String msg) {
         Log.e(TAG, msg);
     }
-
 }
diff --git a/media/java/android/media/MediaSync.java b/media/java/android/media/MediaSync.java
new file mode 100644
index 0000000..e87647d
--- /dev/null
+++ b/media/java/android/media/MediaSync.java
@@ -0,0 +1,526 @@
+/*
+ * 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 android.media;
+
+import android.annotation.IntDef;
+import android.media.AudioTrack;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.Surface;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * MediaSync class can be used to synchronously playback audio and video streams.
+ * It can be used to play audio-only or video-only stream, too.
+ *
+ * <p>MediaSync is generally used like this:
+ * <pre>
+ * MediaSync sync = new MediaSync();
+ * sync.configureSurface(surface);
+ * Surface inputSurface = sync.createInputSurface();
+ * ...
+ * // MediaCodec videoDecoder = ...;
+ * videoDecoder.configure(format, inputSurface, ...);
+ * ...
+ * sync.configureAudioTrack(audioTrack, nativeSampleRateInHz);
+ * sync.setCallback(new MediaSync.Callback() {
+ *     \@Override
+ *     public void onReturnAudioBuffer(MediaSync sync, ByteBuffer audioBuffer, int bufferIndex) {
+ *         ...
+ *     }
+ * });
+ * // This needs to be done since sync is paused on creation.
+ * sync.setPlaybackRate(1.0f, MediaSync.PLAYBACK_RATE_AUDIO_MODE_RESAMPLE);
+ *
+ * for (;;) {
+ *   ...
+ *   // send video frames to surface for rendering, e.g., call
+ *   // videoDecoder.releaseOutputBuffer(videoOutputBufferIx, videoPresentationTimeNs);
+ *   // More details are available as below.
+ *   ...
+ *   sync.queueAudio(audioByteBuffer, bufferIndex, size, audioPresentationTimeUs); // non-blocking.
+ *   // The audioByteBuffer and bufferIndex will be returned via callback.
+ *   // More details are available as below.
+ *   ...
+ *     ...
+ * }
+ * sync.setPlaybackRate(0.0f, MediaSync.PLAYBACK_RATE_AUDIO_MODE_RESAMPLE);
+ * sync.release();
+ * sync = null;
+ *
+ * // The following code snippet illustrates how video/audio raw frames are created by
+ * // MediaCodec's, how they are fed to MediaSync and how they are returned by MediaSync.
+ * // This is the callback from MediaCodec.
+ * onOutputBufferAvailable(MediaCodec codec, int bufferIndex, BufferInfo info) {
+ *     // ...
+ *     if (codec == videoDecoder) {
+ *         // surface timestamp must contain media presentation time in nanoseconds.
+ *         codec.releaseOutputBuffer(bufferIndex, 1000 * info.presentationTime);
+ *     } else {
+ *         ByteBuffer audioByteBuffer = codec.getOutputBuffer(bufferIndex);
+ *         sync.queueByteBuffer(audioByteBuffer, bufferIndex, info.size, info.presentationTime);
+ *     }
+ *     // ...
+ * }
+ *
+ * // This is the callback from MediaSync.
+ * onReturnAudioBuffer(MediaSync sync, ByteBuffer buffer, int bufferIndex) {
+ *     // ...
+ *     audioDecoder.releaseBuffer(bufferIndex, false);
+ *     // ...
+ * }
+ *
+ * </pre>
+ *
+ * The client needs to configure corresponding sink (i.e., Surface and AudioTrack) based on
+ * the stream type it will play.
+ * <p>
+ * For video, the client needs to call {@link #createInputSurface} to obtain a surface on
+ * which it will render video frames.
+ * <p>
+ * For audio, the client needs to set up audio track correctly, e.g., using {@link
+ * AudioTrack#MODE_STREAM}. The audio buffers are sent to MediaSync directly via {@link
+ * #queueAudio}, and are returned to the client via {@link Callback#onReturnAudioBuffer}
+ * asynchronously. The client should not modify an audio buffer till it's returned.
+ * <p>
+ * The client can optionally pre-fill audio/video buffers by setting playback rate to 0.0,
+ * and then feed audio/video buffers to corresponding components. This can reduce possible
+ * initial underrun.
+ * <p>
+ */
+final public class MediaSync {
+    /**
+     * MediaSync callback interface. Used to notify the user asynchronously
+     * of various MediaSync events.
+     */
+    public static abstract class Callback {
+        /**
+         * Called when returning an audio buffer which has been consumed.
+         *
+         * @param sync The MediaSync object.
+         * @param audioBuffer The returned audio buffer.
+         */
+        public abstract void onReturnAudioBuffer(
+                MediaSync sync, ByteBuffer audioBuffer, int bufferIndex);
+    }
+
+    private static final String TAG = "MediaSync";
+
+    private static final int EVENT_CALLBACK = 1;
+    private static final int EVENT_SET_CALLBACK = 2;
+
+    private static final int CB_RETURN_AUDIO_BUFFER = 1;
+
+    private static class AudioBuffer {
+        public ByteBuffer mByteBuffer;
+        public int mBufferIndex;
+        public int mSizeInBytes;
+        long mPresentationTimeUs;
+
+        public AudioBuffer(ByteBuffer byteBuffer, int bufferIndex,
+                           int sizeInBytes, long presentationTimeUs) {
+            mByteBuffer = byteBuffer;
+            mBufferIndex = bufferIndex;
+            mSizeInBytes = sizeInBytes;
+            mPresentationTimeUs = presentationTimeUs;
+        }
+    }
+
+    private final Object mCallbackLock = new Object();
+    private Handler mCallbackHandler = null;
+    private MediaSync.Callback mCallback = null;
+
+    private int mNativeSampleRateInHz = 0;
+
+    private Thread mAudioThread = null;
+    // Created on mAudioThread when mAudioThread is started. When used on user thread, they should
+    // be guarded by checking mAudioThread.
+    private Handler mAudioHandler = null;
+    private Looper mAudioLooper = null;
+
+    private final Object mAudioLock = new Object();
+    private AudioTrack mAudioTrack = null;
+    private List<AudioBuffer> mAudioBuffers = new LinkedList<AudioBuffer>();
+    private float mPlaybackRate = 0.0f;
+
+    private long mNativeContext;
+
+    /**
+     * Class constructor. On creation, MediaSync is paused, i.e., playback rate is 0.0f.
+     */
+    public MediaSync() {
+        native_setup();
+    }
+
+    private native final void native_setup();
+
+    @Override
+    protected void finalize() {
+        native_finalize();
+    }
+
+    private native final void native_finalize();
+
+    /**
+     * Make sure you call this when you're done to free up any opened
+     * component instance instead of relying on the garbage collector
+     * to do this for you at some point in the future.
+     */
+    public final void release() {
+        returnAudioBuffers();
+        if (mAudioThread != null) {
+            if (mAudioLooper != null) {
+                mAudioLooper.quit();
+            }
+        }
+        setCallback(null, null);
+        native_release();
+    }
+
+    private native final void native_release();
+
+    /**
+     * Sets an asynchronous callback for actionable MediaSync events.
+     * It shouldn't be called inside callback.
+     *
+     * @param cb The callback that will run.
+     * @param handler The Handler that will run the callback. Using null means to use MediaSync's
+     *     internal handler if it exists.
+     */
+    public void setCallback(/* MediaSync. */ Callback cb, Handler handler) {
+        synchronized(mCallbackLock) {
+            if (handler != null) {
+                mCallbackHandler = handler;
+            } else {
+                Looper looper;
+                if ((looper = Looper.myLooper()) == null) {
+                    looper = Looper.getMainLooper();
+                }
+                if (looper == null) {
+                    mCallbackHandler = null;
+                } else {
+                    mCallbackHandler = new Handler(looper);
+                }
+            }
+
+            mCallback = cb;
+        }
+    }
+
+    /**
+     * Configures the output surface for MediaSync.
+     *
+     * @param surface Specify a surface on which to render the video data.
+     * @throws IllegalArgumentException if the surface has been released, or is invalid.
+     *     or can not be connected.
+     * @throws IllegalStateException if not in the Initialized state, or another surface
+     *     has already been configured.
+     */
+    public void configureSurface(Surface surface) {
+        native_configureSurface(surface);
+    }
+
+    private native final void native_configureSurface(Surface surface);
+
+    /**
+     * Configures the audio track for MediaSync.
+     *
+     * @param audioTrack Specify an AudioTrack through which to render the audio data.
+     * @throws IllegalArgumentException if the audioTrack has been released, or is invalid,
+     *     or nativeSampleRateInHz is invalid.
+     * @throws IllegalStateException if not in the Initialized state, or another audio track
+     *     has already been configured.
+     */
+    public void configureAudioTrack(AudioTrack audioTrack, int nativeSampleRateInHz) {
+        if (audioTrack != null && nativeSampleRateInHz <= 0) {
+            final String msg = "Native sample rate " + nativeSampleRateInHz + " is invalid";
+            throw new IllegalArgumentException(msg);
+        }
+        native_configureAudioTrack(audioTrack, nativeSampleRateInHz);
+        mAudioTrack = audioTrack;
+        mNativeSampleRateInHz = nativeSampleRateInHz;
+        if (mAudioThread == null) {
+            createAudioThread();
+        }
+    }
+
+    private native final void native_configureAudioTrack(
+            AudioTrack audioTrack, int nativeSampleRateInHz);
+
+    /**
+     * Requests a Surface to use as the input. This may only be called after
+     * {@link #configureSurface}.
+     * <p>
+     * The application is responsible for calling release() on the Surface when
+     * done.
+     * @throws IllegalStateException if not configured, or another input surface has
+     *     already been created.
+     */
+    public native final Surface createInputSurface();
+
+    /**
+     * Specifies resampling as audio mode for variable rate playback, i.e.,
+     * resample the waveform based on the requested playback rate to get
+     * a new waveform, and play back the new waveform at the original sampling
+     * frequency.
+     * When rate is larger than 1.0, pitch becomes higher.
+     * When rate is smaller than 1.0, pitch becomes lower.
+     */
+    public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 0;
+
+    /**
+     * Specifies time stretching as audio mode for variable rate playback.
+     * Time stretching changes the duration of the audio samples without
+     * affecting its pitch.
+     * FIXME: implement time strectching.
+     * @hide
+     */
+    public static final int PLAYBACK_RATE_AUDIO_MODE_STRETCH = 1;
+
+    /** @hide */
+    @IntDef(
+        value = {
+            PLAYBACK_RATE_AUDIO_MODE_RESAMPLE,
+            PLAYBACK_RATE_AUDIO_MODE_STRETCH })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PlaybackRateAudioMode {}
+
+    /**
+     * Sets playback rate. It does same as {@link #setPlaybackRate(float, int)},
+     * except that it always uses {@link #PLAYBACK_RATE_AUDIO_MODE_STRETCH} for audioMode.
+     *
+     * @param rate the ratio between desired playback rate and normal one. 1.0 means normal
+     *     playback speed. 0.0 means stop or pause. Value larger than 1.0 means faster playback,
+     *     while value between 0.0 and 1.0 for slower playback.
+     *
+     * @throws IllegalStateException if the internal sync engine or the audio track has not
+     *     been initialized.
+     * TODO: unhide when PLAYBACK_RATE_AUDIO_MODE_STRETCH is supported.
+     * @hide
+     */
+    public void setPlaybackRate(float rate) {
+        setPlaybackRate(rate, PLAYBACK_RATE_AUDIO_MODE_STRETCH);
+    }
+
+    /**
+     * Sets playback rate and audio mode.
+     *
+     * <p> The supported audio modes are:
+     * <ul>
+     * <li> {@link #PLAYBACK_RATE_AUDIO_MODE_RESAMPLE}
+     * </ul>
+     *
+     * @param rate the ratio between desired playback rate and normal one. 1.0 means normal
+     *     playback speed. 0.0 means stop or pause. Value larger than 1.0 means faster playback,
+     *     while value between 0.0 and 1.0 for slower playback.
+     * @param audioMode audio playback mode. Must be one of the supported
+     *     audio modes.
+     *
+     * @throws IllegalStateException if the internal sync engine or the audio track has not
+     *     been initialized.
+     * @throws IllegalArgumentException if audioMode is not supported.
+     */
+    public void setPlaybackRate(float rate, @PlaybackRateAudioMode int audioMode) {
+        if (!isAudioPlaybackModeSupported(audioMode)) {
+            final String msg = "Audio playback mode " + audioMode + " is not supported";
+            throw new IllegalArgumentException(msg);
+        }
+
+        int status = AudioTrack.SUCCESS;
+        if (mAudioTrack != null) {
+            int playbackSampleRate = (int)(rate * mNativeSampleRateInHz + 0.5);
+            rate = playbackSampleRate / (float)mNativeSampleRateInHz;
+
+            try {
+                if (rate == 0.0) {
+                    mAudioTrack.pause();
+                } else {
+                    status = mAudioTrack.setPlaybackRate(playbackSampleRate);
+                    mAudioTrack.play();
+                }
+            } catch (IllegalStateException e) {
+                throw e;
+            }
+        }
+
+        if (status != AudioTrack.SUCCESS) {
+            throw new IllegalArgumentException("Fail to set playback rate in audio track");
+        }
+
+        synchronized(mAudioLock) {
+            mPlaybackRate = rate;
+        }
+        if (mPlaybackRate != 0.0 && mAudioThread != null) {
+            postRenderAudio(0);
+        }
+        native_setPlaybackRate(mPlaybackRate);
+    }
+
+    private native final void native_setPlaybackRate(float rate);
+
+    /*
+     * Test whether a given audio playback mode is supported.
+     * TODO query supported AudioPlaybackMode from audio track.
+     */
+    private boolean isAudioPlaybackModeSupported(int mode) {
+        return (mode == PLAYBACK_RATE_AUDIO_MODE_RESAMPLE);
+    }
+
+    /**
+     * Queues the audio data asynchronously for playback (AudioTrack must be in streaming mode).
+     * @param audioData the buffer that holds the data to play. This buffer will be returned
+     *     to the client via registered callback.
+     * @param bufferIndex the buffer index used to identify audioData. It will be returned to
+     *     the client along with audioData. This helps applications to keep track of audioData.
+     * @param sizeInBytes number of bytes to queue.
+     * @param presentationTimeUs the presentation timestamp in microseconds for the first frame
+     *     in the buffer.
+     * @throws IllegalStateException if audio track is not configured or internal configureation
+     *     has not been done correctly.
+     */
+    public void queueAudio(
+            ByteBuffer audioData, int bufferIndex, int sizeInBytes, long presentationTimeUs) {
+        if (mAudioTrack == null || mAudioThread == null) {
+            throw new IllegalStateException(
+                    "AudioTrack is NOT configured or audio thread is not created");
+        }
+
+        synchronized(mAudioLock) {
+            mAudioBuffers.add(new AudioBuffer(
+                    audioData, bufferIndex, sizeInBytes, presentationTimeUs));
+        }
+
+        if (mPlaybackRate != 0.0) {
+            postRenderAudio(0);
+        }
+    }
+
+    // When called on user thread, make sure to check mAudioThread != null.
+    private void postRenderAudio(long delayMillis) {
+        mAudioHandler.postDelayed(new Runnable() {
+            public void run() {
+                synchronized(mAudioLock) {
+                    if (mPlaybackRate == 0.0) {
+                        return;
+                    }
+
+                    if (mAudioBuffers.isEmpty()) {
+                        return;
+                    }
+
+                    AudioBuffer audioBuffer = mAudioBuffers.get(0);
+                    int sizeWritten = mAudioTrack.write(
+                            audioBuffer.mByteBuffer,
+                            audioBuffer.mSizeInBytes,
+                            AudioTrack.WRITE_NON_BLOCKING);
+                    if (sizeWritten > 0) {
+                        if (audioBuffer.mPresentationTimeUs != -1) {
+                            native_updateQueuedAudioData(
+                                    audioBuffer.mSizeInBytes, audioBuffer.mPresentationTimeUs);
+                            audioBuffer.mPresentationTimeUs = -1;
+                        }
+
+                        if (sizeWritten == audioBuffer.mSizeInBytes) {
+                            postReturnByteBuffer(audioBuffer);
+                            mAudioBuffers.remove(0);
+                            if (!mAudioBuffers.isEmpty()) {
+                                postRenderAudio(0);
+                            }
+                            return;
+                        }
+
+                        audioBuffer.mSizeInBytes -= sizeWritten;
+                    }
+                    // TODO: wait time depends on fullness of audio track.
+                    postRenderAudio(10);
+                }
+            }
+        }, delayMillis);
+    }
+
+    private native final void native_updateQueuedAudioData(
+            int sizeInBytes, long presentationTimeUs);
+
+    private final void postReturnByteBuffer(final AudioBuffer audioBuffer) {
+        synchronized(mCallbackLock) {
+            if (mCallbackHandler != null) {
+                final MediaSync sync = this;
+                mCallbackHandler.post(new Runnable() {
+                    public void run() {
+                        synchronized(mCallbackLock) {
+                            if (mCallbackHandler == null
+                                    || mCallbackHandler.getLooper().getThread()
+                                            != Thread.currentThread()) {
+                                // callback handler has been changed.
+                                return;
+                            }
+                            if (mCallback != null) {
+                                mCallback.onReturnAudioBuffer(sync, audioBuffer.mByteBuffer,
+                                        audioBuffer.mBufferIndex);
+                            }
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    private final void returnAudioBuffers() {
+        synchronized(mAudioLock) {
+            for (AudioBuffer audioBuffer: mAudioBuffers) {
+                postReturnByteBuffer(audioBuffer);
+            }
+            mAudioBuffers.clear();
+        }
+    }
+
+    private void createAudioThread() {
+        mAudioThread = new Thread() {
+            @Override
+            public void run() {
+                Looper.prepare();
+                synchronized(mAudioLock) {
+                    mAudioLooper = Looper.myLooper();
+                    mAudioHandler = new Handler();
+                    mAudioLock.notify();
+                }
+                Looper.loop();
+            }
+        };
+        mAudioThread.start();
+
+        synchronized(mAudioLock) {
+            try {
+                mAudioLock.wait();
+            } catch(InterruptedException e) {
+            }
+        }
+    }
+
+    static {
+        System.loadLibrary("media_jni");
+        native_init();
+    }
+
+    private static native final void native_init();
+}
diff --git a/media/jni/Android.mk b/media/jni/Android.mk
index dae57a8..5b177e5 100644
--- a/media/jni/Android.mk
+++ b/media/jni/Android.mk
@@ -10,11 +10,12 @@
     android_media_MediaDrm.cpp \
     android_media_MediaExtractor.cpp \
     android_media_MediaHTTPConnection.cpp \
+    android_media_MediaMetadataRetriever.cpp \
     android_media_MediaMuxer.cpp \
     android_media_MediaPlayer.cpp \
     android_media_MediaRecorder.cpp \
     android_media_MediaScanner.cpp \
-    android_media_MediaMetadataRetriever.cpp \
+    android_media_MediaSync.cpp \
     android_media_ResampleInputStream.cpp \
     android_media_MediaProfiles.cpp \
     android_media_AmrInputStream.cpp \
diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp
index b748f3a..3e41716 100644
--- a/media/jni/android_media_MediaPlayer.cpp
+++ b/media/jni/android_media_MediaPlayer.cpp
@@ -926,6 +926,7 @@
 extern int register_android_media_MediaMuxer(JNIEnv *env);
 extern int register_android_media_MediaRecorder(JNIEnv *env);
 extern int register_android_media_MediaScanner(JNIEnv *env);
+extern int register_android_media_MediaSync(JNIEnv *env);
 extern int register_android_media_ResampleInputStream(JNIEnv *env);
 extern int register_android_media_MediaProfiles(JNIEnv *env);
 extern int register_android_media_AmrInputStream(JNIEnv *env);
@@ -1009,6 +1010,11 @@
         goto bail;
     }
 
+    if (register_android_media_MediaSync(env) < 0) {
+        ALOGE("ERROR: MediaSync native registration failed");
+        goto bail;
+    }
+
     if (register_android_media_MediaExtractor(env) < 0) {
         ALOGE("ERROR: MediaCodec native registration failed");
         goto bail;
diff --git a/media/jni/android_media_MediaSync.cpp b/media/jni/android_media_MediaSync.cpp
new file mode 100644
index 0000000..f31b511
--- /dev/null
+++ b/media/jni/android_media_MediaSync.cpp
@@ -0,0 +1,284 @@
+/*
+ * Copyright 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "MediaSync-JNI"
+#include <utils/Log.h>
+
+#include "android_media_MediaSync.h"
+
+#include "android_media_AudioTrack.h"
+#include "android_runtime/AndroidRuntime.h"
+#include "android_runtime/android_view_Surface.h"
+#include "jni.h"
+#include "JNIHelp.h"
+
+#include <gui/Surface.h>
+
+#include <media/AudioTrack.h>
+#include <media/stagefright/MediaSync.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/AString.h>
+
+#include <nativehelper/ScopedLocalRef.h>
+
+namespace android {
+
+struct fields_t {
+    jfieldID context;
+};
+
+static fields_t gFields;
+
+////////////////////////////////////////////////////////////////////////////////
+
+JMediaSync::JMediaSync() {
+    mSync = MediaSync::create();
+}
+
+JMediaSync::~JMediaSync() {
+}
+
+status_t JMediaSync::configureSurface(const sp<IGraphicBufferProducer> &bufferProducer) {
+    return mSync->configureSurface(bufferProducer);
+}
+
+status_t JMediaSync::configureAudioTrack(
+        const sp<AudioTrack> &audioTrack,
+        int32_t nativeSampleRateInHz) {
+    return mSync->configureAudioTrack(audioTrack, nativeSampleRateInHz);
+}
+
+status_t JMediaSync::createInputSurface(
+        sp<IGraphicBufferProducer>* bufferProducer) {
+    return mSync->createInputSurface(bufferProducer);
+}
+
+void JMediaSync::setPlaybackRate(float rate) {
+    mSync->setPlaybackRate(rate);
+}
+
+status_t JMediaSync::updateQueuedAudioData(
+        int sizeInBytes, int64_t presentationTimeUs) {
+    return mSync->updateQueuedAudioData(sizeInBytes, presentationTimeUs);
+}
+
+}  // namespace android
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace android;
+
+static sp<JMediaSync> setMediaSync(JNIEnv *env, jobject thiz, const sp<JMediaSync> &sync) {
+    sp<JMediaSync> old = (JMediaSync *)env->GetLongField(thiz, gFields.context);
+    if (sync != NULL) {
+        sync->incStrong(thiz);
+    }
+    if (old != NULL) {
+        old->decStrong(thiz);
+    }
+
+    env->SetLongField(thiz, gFields.context, (jlong)sync.get());
+
+    return old;
+}
+
+static sp<JMediaSync> getMediaSync(JNIEnv *env, jobject thiz) {
+    return (JMediaSync *)env->GetLongField(thiz, gFields.context);
+}
+
+static void android_media_MediaSync_release(JNIEnv *env, jobject thiz) {
+    setMediaSync(env, thiz, NULL);
+}
+
+static void throwExceptionAsNecessary(
+        JNIEnv *env, status_t err, const char *msg = NULL) {
+    switch (err) {
+        case INVALID_OPERATION:
+            jniThrowException(env, "java/lang/IllegalStateException", msg);
+            break;
+
+        case BAD_VALUE:
+            jniThrowException(env, "java/lang/IllegalArgumentException", msg);
+            break;
+
+        default:
+            break;
+    }
+}
+
+static void android_media_MediaSync_native_configureSurface(
+        JNIEnv *env, jobject thiz, jobject jsurface) {
+    ALOGV("android_media_MediaSync_configureSurface");
+
+    sp<JMediaSync> sync = getMediaSync(env, thiz);
+    if (sync == NULL) {
+        throwExceptionAsNecessary(env, INVALID_OPERATION);
+        return;
+    }
+
+    sp<IGraphicBufferProducer> bufferProducer;
+    if (jsurface != NULL) {
+        sp<Surface> surface(android_view_Surface_getSurface(env, jsurface));
+        if (surface != NULL) {
+            bufferProducer = surface->getIGraphicBufferProducer();
+        } else {
+            throwExceptionAsNecessary(env, BAD_VALUE, "The surface has been released");
+            return;
+        }
+    }
+
+    status_t err = sync->configureSurface(bufferProducer);
+
+    if (err == INVALID_OPERATION) {
+        throwExceptionAsNecessary(
+                env, INVALID_OPERATION, "Surface has already been configured");
+    } if (err != NO_ERROR) {
+        AString msg("Failed to connect to surface with error ");
+        msg.append(err);
+        throwExceptionAsNecessary(env, BAD_VALUE, msg.c_str());
+    }
+}
+
+static void android_media_MediaSync_native_configureAudioTrack(
+        JNIEnv *env, jobject thiz, jobject jaudioTrack, jint nativeSampleRateInHz) {
+    ALOGV("android_media_MediaSync_configureAudioTrack");
+
+    sp<JMediaSync> sync = getMediaSync(env, thiz);
+    if (sync == NULL) {
+        throwExceptionAsNecessary(env, INVALID_OPERATION);
+        return;
+    }
+
+    sp<AudioTrack> audioTrack;
+    if (jaudioTrack != NULL) {
+        audioTrack = android_media_AudioTrack_getAudioTrack(env, jaudioTrack);
+        if (audioTrack == NULL) {
+            throwExceptionAsNecessary(env, BAD_VALUE, "The audio track has been released");
+            return;
+        }
+    }
+
+    status_t err = sync->configureAudioTrack(audioTrack, nativeSampleRateInHz);
+
+    if (err == INVALID_OPERATION) {
+        throwExceptionAsNecessary(
+                env, INVALID_OPERATION, "Audio track has already been configured");
+    } if (err != NO_ERROR) {
+        AString msg("Failed to configure audio track with error ");
+        msg.append(err);
+        throwExceptionAsNecessary(env, BAD_VALUE, msg.c_str());
+    }
+}
+
+static jobject android_media_MediaSync_createInputSurface(
+        JNIEnv* env, jobject thiz) {
+    ALOGV("android_media_MediaSync_createInputSurface");
+
+    sp<JMediaSync> sync = getMediaSync(env, thiz);
+    if (sync == NULL) {
+        throwExceptionAsNecessary(env, INVALID_OPERATION);
+        return NULL;
+    }
+
+    // Tell the MediaSync that we want to use a Surface as input.
+    sp<IGraphicBufferProducer> bufferProducer;
+    status_t err = sync->createInputSurface(&bufferProducer);
+    if (err != NO_ERROR) {
+        throwExceptionAsNecessary(env, INVALID_OPERATION);
+        return NULL;
+    }
+
+    // Wrap the IGBP in a Java-language Surface.
+    return android_view_Surface_createFromIGraphicBufferProducer(env,
+            bufferProducer);
+}
+
+static void android_media_MediaSync_native_updateQueuedAudioData(
+        JNIEnv *env, jobject thiz, jint sizeInBytes, jlong presentationTimeUs) {
+    sp<JMediaSync> sync = getMediaSync(env, thiz);
+    if (sync == NULL) {
+        throwExceptionAsNecessary(env, INVALID_OPERATION);
+        return;
+    }
+
+    status_t err = sync->updateQueuedAudioData(sizeInBytes, presentationTimeUs);
+    if (err != NO_ERROR) {
+        throwExceptionAsNecessary(env, err);
+        return;
+    }
+}
+
+static void android_media_MediaSync_native_init(JNIEnv *env) {
+    ScopedLocalRef<jclass> clazz(env, env->FindClass("android/media/MediaSync"));
+    CHECK(clazz.get() != NULL);
+
+    gFields.context = env->GetFieldID(clazz.get(), "mNativeContext", "J");
+    CHECK(gFields.context != NULL);
+}
+
+static void android_media_MediaSync_native_setup(JNIEnv *env, jobject thiz) {
+    sp<JMediaSync> sync = new JMediaSync();
+
+    setMediaSync(env, thiz, sync);
+}
+
+static void android_media_MediaSync_native_setPlaybackRate(
+        JNIEnv *env, jobject thiz, jfloat rate) {
+    sp<JMediaSync> sync = getMediaSync(env, thiz);
+    if (sync == NULL) {
+        throwExceptionAsNecessary(env, INVALID_OPERATION);
+        return;
+    }
+
+    sync->setPlaybackRate(rate);
+}
+
+static void android_media_MediaSync_native_finalize(JNIEnv *env, jobject thiz) {
+    android_media_MediaSync_release(env, thiz);
+}
+
+static JNINativeMethod gMethods[] = {
+    { "native_configureSurface",
+      "(Landroid/view/Surface;)V",
+      (void *)android_media_MediaSync_native_configureSurface },
+
+    { "native_configureAudioTrack",
+      "(Landroid/media/AudioTrack;I)V",
+      (void *)android_media_MediaSync_native_configureAudioTrack },
+
+    { "createInputSurface", "()Landroid/view/Surface;",
+      (void *)android_media_MediaSync_createInputSurface },
+
+    { "native_updateQueuedAudioData",
+      "(IJ)V",
+      (void *)android_media_MediaSync_native_updateQueuedAudioData },
+
+    { "native_init", "()V", (void *)android_media_MediaSync_native_init },
+
+    { "native_setup", "()V", (void *)android_media_MediaSync_native_setup },
+
+    { "native_release", "()V", (void *)android_media_MediaSync_release },
+
+    { "native_setPlaybackRate", "(F)V", (void *)android_media_MediaSync_native_setPlaybackRate },
+
+    { "native_finalize", "()V", (void *)android_media_MediaSync_native_finalize },
+};
+
+int register_android_media_MediaSync(JNIEnv *env) {
+    return AndroidRuntime::registerNativeMethods(
+                   env, "android/media/MediaSync", gMethods, NELEM(gMethods));
+}
diff --git a/media/jni/android_media_MediaSync.h b/media/jni/android_media_MediaSync.h
new file mode 100644
index 0000000..5750083
--- /dev/null
+++ b/media/jni/android_media_MediaSync.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 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_MEDIA_MEDIASYNC_H_
+#define _ANDROID_MEDIA_MEDIASYNC_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+namespace android {
+
+class AudioTrack;
+struct IGraphicBufferProducer;
+class MediaSync;
+
+struct JMediaSync : public RefBase {
+    JMediaSync();
+
+    status_t configureSurface(const sp<IGraphicBufferProducer> &bufferProducer);
+    status_t configureAudioTrack(
+            const sp<AudioTrack> &audioTrack, int32_t nativeSampleRateInHz);
+
+    status_t createInputSurface(sp<IGraphicBufferProducer>* bufferProducer);
+
+    status_t updateQueuedAudioData(int sizeInBytes, int64_t presentationTimeUs);
+
+    void setPlaybackRate(float rate);
+
+protected:
+    virtual ~JMediaSync();
+
+private:
+    sp<MediaSync> mSync;
+
+    DISALLOW_EVIL_CONSTRUCTORS(JMediaSync);
+};
+
+}  // namespace android
+
+#endif  // _ANDROID_MEDIA_MEDIASYNC_H_
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 165b11e..a6f7a26 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -65,5 +65,10 @@
                 <data android:scheme="package" />
             </intent-filter>
         </receiver>
+
+        <service
+            android:name=".CopyService"
+            android:exported="false">
+        </service>
     </application>
 </manifest>
diff --git a/packages/DocumentsUI/res/menu/mode_directory.xml b/packages/DocumentsUI/res/menu/mode_directory.xml
index 695060d..4b89823 100644
--- a/packages/DocumentsUI/res/menu/mode_directory.xml
+++ b/packages/DocumentsUI/res/menu/mode_directory.xml
@@ -33,4 +33,8 @@
         android:id="@+id/menu_select_all"
         android:title="@string/menu_select_all"
         android:showAsAction="never" />
+    <item
+        android:id="@+id/menu_copy"
+        android:title="@string/menu_copy"
+        android:showAsAction="never" />
 </menu>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 4ad337d..310ccf0 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -48,6 +48,8 @@
     <string name="menu_select">Select \"<xliff:g id="directory" example="My Directory">^1</xliff:g>\"</string>
     <!-- Menu item title that selects all documents in the current directory [CHAR LIMIT=24] -->
     <string name="menu_select_all">Select All</string>
+    <!-- Menu item title that copies the selected documents [CHAR LIMIT=24] -->
+    <string name="menu_copy">Copy to\u2026</string>
 
     <!-- Menu item that reveals internal storage built into the device [CHAR LIMIT=24] -->
     <string name="menu_advanced_show" product="nosdcard">Show internal storage</string>
@@ -110,4 +112,16 @@
     <!-- Title of dialog when prompting user to select an app to share documents with [CHAR LIMIT=32] -->
     <string name="share_via">Share via</string>
 
+    <!-- Title of the cancel button [CHAR LIMIT=24] -->
+    <string name="cancel">Cancel</string>
+    <!-- Title of the copy notification [CHAR LIMIT=24] -->
+    <string name="copy_notification_title">Copying files</string>
+    <!-- Text shown on the copy notification to indicate remaining time, in minutes [CHAR LIMIT=24] -->
+    <string name="copy_remaining"><xliff:g id="duration" example="3 minutes">%s</xliff:g> left</string>
+    <!-- Toast shown when a file copy is kicked off -->
+    <plurals name="copy_begin">
+      <item quantity="one">Copying <xliff:g id="count" example="1">%1$d</xliff:g> file.</item>
+      <item quantity="other">Copying <xliff:g id="count" example="3">%1$d</xliff:g> files.</item>
+    </plurals>
+
 </resources>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java
new file mode 100644
index 0000000..f7d8cc4
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java
@@ -0,0 +1,280 @@
+/*
+ * 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.documentsui;
+
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+import com.android.documentsui.model.DocumentInfo;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+
+public class CopyService extends IntentService {
+    public static final String TAG = "CopyService";
+    public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
+    private static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
+
+    private NotificationManager mNotificationManager;
+    private Notification.Builder mProgressBuilder;
+
+    // Jobs are serialized but a job ID is used, to avoid mixing up cancellation requests.
+    private String mJobId;
+    private volatile boolean mIsCancelled;
+    // Parameters of the copy job. Requests to an IntentService are serialized so this code only
+    // needs to deal with one job at a time.
+    private long mBatchSize;
+    private long mBytesCopied;
+    private long mStartTime;
+    private long mLastNotificationTime;
+    // Speed estimation
+    private long mBytesCopiedSample;
+    private long mSampleTime;
+    private long mSpeed;
+    private long mRemainingTime;
+
+    public CopyService() {
+        super("CopyService");
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (intent.hasExtra(EXTRA_CANCEL)) {
+            handleCancel(intent);
+        }
+        return super.onStartCommand(intent, flags, startId);
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        if (intent.hasExtra(EXTRA_CANCEL)) {
+            handleCancel(intent);
+            return;
+        }
+
+        ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
+        // Use the app local files dir as a copy destination for now. This resolves to
+        // /data/data/com.android.documentsui/files.
+        // TODO: Add actual destination picking.
+        File destinationDir = getFilesDir();
+
+        setupCopyJob(srcs, destinationDir);
+
+        ArrayList<String> failedFilenames = new ArrayList<String>();
+        for (int i = 0; i < srcs.size() && !mIsCancelled; ++i) {
+            DocumentInfo src = srcs.get(i);
+            try {
+                copyFile(src, destinationDir);
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to copy " + src.displayName, e);
+                failedFilenames.add(src.displayName);
+            }
+        }
+
+        if (failedFilenames.size() > 0) {
+            // TODO: Display a notification when an error has occurred.
+        }
+
+        // Dismiss the ongoing copy notification when the copy is done.
+        mNotificationManager.cancel(mJobId, 0);
+
+        // TODO: Display a toast if the copy was cancelled.
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    /**
+     * Sets up the CopyService to start tracking and sending notifications for the given batch of
+     * files.
+     *
+     * @param srcs A list of src files to copy.
+     */
+    private void setupCopyJob(ArrayList<DocumentInfo> srcs, File destinationDir) {
+        // Create an ID for this copy job. Use the timestamp.
+        mJobId = String.valueOf(SystemClock.elapsedRealtime());
+        // Reset the cancellation flag.
+        mIsCancelled = false;
+
+        mProgressBuilder = new Notification.Builder(this)
+                .setContentTitle(getString(R.string.copy_notification_title))
+                .setCategory(Notification.CATEGORY_PROGRESS)
+                .setSmallIcon(R.drawable.ic_menu_copy).setOngoing(true);
+
+        Intent cancelIntent = new Intent(this, CopyService.class);
+        cancelIntent.putExtra(EXTRA_CANCEL, mJobId);
+        mProgressBuilder.addAction(R.drawable.ic_cab_cancel,
+                getString(R.string.cancel), PendingIntent.getService(this, 0,
+                        cancelIntent, PendingIntent.FLAG_ONE_SHOT));
+
+        // TODO: Add a content intent to open the destination folder.
+
+        // Send an initial progress notification.
+        mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
+
+        // Reset batch parameters.
+        mBatchSize = 0;
+        for (DocumentInfo doc : srcs) {
+            mBatchSize += doc.size;
+        }
+        mBytesCopied = 0;
+        mStartTime = SystemClock.elapsedRealtime();
+        mLastNotificationTime = 0;
+        mBytesCopiedSample = 0;
+        mSampleTime = 0;
+        mSpeed = 0;
+        mRemainingTime = 0;
+
+        // TODO: Check preconditions for copy.
+        // - check that the destination has enough space and is writeable?
+        // - check MIME types?
+    }
+
+    /**
+     * Cancels the current copy job, if its ID matches the given ID.
+     *
+     * @param intent The cancellation intent.
+     */
+    private void handleCancel(Intent intent) {
+        final String cancelledId = intent.getStringExtra(EXTRA_CANCEL);
+        // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
+        // cancellation requests from affecting unrelated copy jobs.
+        if (java.util.Objects.equals(mJobId, cancelledId)) {
+            // Set the cancel flag. This causes the copy loops to exit.
+            mIsCancelled = true;
+            // Dismiss the progress notification here rather than in the copy loop. This preserves
+            // interactivity for the user in case the copy loop is stalled.
+            mNotificationManager.cancel(mJobId, 0);
+        }
+    }
+
+    /**
+     * Logs progress on the current copy operation. Displays/Updates the progress notification.
+     *
+     * @param bytesCopied
+     */
+    private void makeProgress(long bytesCopied) {
+        mBytesCopied += bytesCopied;
+        double done = (double) mBytesCopied / mBatchSize;
+        String percent = NumberFormat.getPercentInstance().format(done);
+
+        // Update time estimate
+        long currentTime = SystemClock.elapsedRealtime();
+        long elapsedTime = currentTime - mStartTime;
+
+        // Send out progress notifications once a second.
+        if (currentTime - mLastNotificationTime > 1000) {
+            updateRemainingTimeEstimate(elapsedTime);
+            mProgressBuilder.setProgress(100, (int) (done * 100), false);
+            mProgressBuilder.setContentInfo(percent);
+            if (mRemainingTime > 0) {
+                mProgressBuilder.setContentText(getString(R.string.copy_remaining,
+                        DateUtils.formatDuration(mRemainingTime)));
+            } else {
+                mProgressBuilder.setContentText(null);
+            }
+            mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
+            mLastNotificationTime = currentTime;
+        }
+    }
+
+    /**
+     * Generates an estimate of the remaining time in the copy.
+     *
+     * @param elapsedTime The time elapsed so far.
+     */
+    private void updateRemainingTimeEstimate(long elapsedTime) {
+        final long sampleDuration = elapsedTime - mSampleTime;
+        final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
+        if (mSpeed == 0) {
+            mSpeed = sampleSpeed;
+        } else {
+            mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
+        }
+
+        if (mSampleTime > 0 && mSpeed > 0) {
+            mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
+        } else {
+            mRemainingTime = 0;
+        }
+
+        mSampleTime = elapsedTime;
+        mBytesCopiedSample = mBytesCopied;
+    }
+
+    /**
+     * Copies a file to a given location.
+     *
+     * @param srcInfo The source file.
+     * @param destination The directory to copy into.
+     * @throws IOException
+     */
+    private void copyFile(DocumentInfo srcInfo, File destinationDir)
+            throws IOException {
+        final Context context = getApplicationContext();
+        final ContentResolver resolver = context.getContentResolver();
+        final File destinationFile = new File(destinationDir, srcInfo.displayName);
+        final Uri destinationUri = Uri.fromFile(destinationFile);
+
+        InputStream source = null;
+        OutputStream destination = null;
+
+        boolean errorOccurred = false;
+        try {
+            source = resolver.openInputStream(srcInfo.derivedUri);
+            destination = resolver.openOutputStream(destinationUri);
+
+            byte[] buffer = new byte[8192];
+            int len;
+            while (!mIsCancelled && ((len = source.read(buffer)) != -1)) {
+                destination.write(buffer, 0, len);
+                makeProgress(len);
+            }
+        } catch (IOException e) {
+            errorOccurred = true;
+            Log.e(TAG, "Error while copying " + srcInfo.displayName, e);
+        } finally {
+            IoUtils.closeQuietly(source);
+            IoUtils.closeQuietly(destination);
+        }
+
+        if (errorOccurred || mIsCancelled) {
+            // Clean up half-copied files.
+            if (!destinationFile.delete()) {
+                Log.w(TAG, "Failed to clean up partially copied file " + srcInfo.displayName);
+            }
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index a75dc42..d6d691f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -50,6 +50,7 @@
 import android.os.CancellationSignal;
 import android.os.OperationCanceledException;
 import android.os.Parcelable;
+import android.os.SystemProperties;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.text.format.DateUtils;
@@ -77,6 +78,7 @@
 import android.widget.Toast;
 
 import com.android.documentsui.BaseActivity.State;
+import com.android.documentsui.CopyService;
 import com.android.documentsui.ProviderExecutor.Preemptable;
 import com.android.documentsui.RecentsProvider.StateColumns;
 import com.android.documentsui.model.DocumentInfo;
@@ -463,11 +465,14 @@
             final MenuItem open = menu.findItem(R.id.menu_open);
             final MenuItem share = menu.findItem(R.id.menu_share);
             final MenuItem delete = menu.findItem(R.id.menu_delete);
+            final MenuItem copy = menu.findItem(R.id.menu_copy);
 
             final boolean manageMode = state.action == ACTION_MANAGE;
             open.setVisible(!manageMode);
             share.setVisible(manageMode);
             delete.setVisible(manageMode);
+            // Hide the copy feature by default.
+            copy.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_copy", false));
 
             return true;
         }
@@ -501,6 +506,11 @@
                 mode.finish();
                 return true;
 
+            } else if (id == R.id.menu_copy) {
+                onCopyDocuments(docs);
+                mode.finish();
+                return true;
+
             } else if (id == R.id.menu_select_all) {
                 int count = mCurrentView.getCount();
                 for (int i = 0; i < count; i++) {
@@ -623,6 +633,20 @@
         }
     }
 
+    private void onCopyDocuments(List<DocumentInfo> docs) {
+        final Context context = getActivity();
+        final Resources res = context.getResources();
+
+        Intent copyIntent = new Intent(context, CopyService.class);
+        copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST,
+                new ArrayList<DocumentInfo>(docs));
+
+        Toast.makeText(context,
+                res.getQuantityString(R.plurals.copy_begin, docs.size(), docs.size()),
+                Toast.LENGTH_SHORT).show();
+        context.startService(copyIntent);
+    }
+
     private static State getDisplayState(Fragment fragment) {
         return ((BaseActivity) fragment.getActivity()).getDisplayState();
     }
diff --git a/services/core/java/com/android/server/MmsServiceBroker.java b/services/core/java/com/android/server/MmsServiceBroker.java
index 0de6a03..9409615 100644
--- a/services/core/java/com/android/server/MmsServiceBroker.java
+++ b/services/core/java/com/android/server/MmsServiceBroker.java
@@ -36,6 +36,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.service.carrier.CarrierMessagingService;
+import android.telephony.SmsManager;
 import android.telephony.TelephonyManager;
 import android.util.Slog;
 
@@ -111,6 +112,106 @@
         }
     };
 
+    // Instance of IMms for returning failure to service API caller,
+    // used when MmsService cannot be connected.
+    private final IMms mServiceStubForFailure = new IMms() {
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+
+        @Override
+        public void sendMessage(int subId, String callingPkg, Uri contentUri, String locationUrl,
+                Bundle configOverrides, PendingIntent sentIntent) throws RemoteException {
+            returnPendingIntentWithError(sentIntent);
+        }
+
+        @Override
+        public void downloadMessage(int subId, String callingPkg, String locationUrl,
+                Uri contentUri, Bundle configOverrides, PendingIntent downloadedIntent)
+                throws RemoteException {
+            returnPendingIntentWithError(downloadedIntent);
+        }
+
+        @Override
+        public Bundle getCarrierConfigValues(int subId) throws RemoteException {
+            return null;
+        }
+
+        @Override
+        public Uri importTextMessage(String callingPkg, String address, int type, String text,
+                long timestampMillis, boolean seen, boolean read) throws RemoteException {
+            return null;
+        }
+
+        @Override
+        public Uri importMultimediaMessage(String callingPkg, Uri contentUri, String messageId,
+                long timestampSecs, boolean seen, boolean read) throws RemoteException {
+            return null;
+        }
+
+        @Override
+        public boolean deleteStoredMessage(String callingPkg, Uri messageUri)
+                throws RemoteException {
+            return false;
+        }
+
+        @Override
+        public boolean deleteStoredConversation(String callingPkg, long conversationId)
+                throws RemoteException {
+            return false;
+        }
+
+        @Override
+        public boolean updateStoredMessageStatus(String callingPkg, Uri messageUri,
+                ContentValues statusValues) throws RemoteException {
+            return false;
+        }
+
+        @Override
+        public boolean archiveStoredConversation(String callingPkg, long conversationId,
+                boolean archived) throws RemoteException {
+            return false;
+        }
+
+        @Override
+        public Uri addTextMessageDraft(String callingPkg, String address, String text)
+                throws RemoteException {
+            return null;
+        }
+
+        @Override
+        public Uri addMultimediaMessageDraft(String callingPkg, Uri contentUri)
+                throws RemoteException {
+            return null;
+        }
+
+        @Override
+        public void sendStoredMessage(int subId, String callingPkg, Uri messageUri,
+                Bundle configOverrides, PendingIntent sentIntent) throws RemoteException {
+            returnPendingIntentWithError(sentIntent);
+        }
+
+        @Override
+        public void setAutoPersisting(String callingPkg, boolean enabled) throws RemoteException {
+            // Do nothing
+        }
+
+        @Override
+        public boolean getAutoPersisting() throws RemoteException {
+            return false;
+        }
+
+        private void returnPendingIntentWithError(PendingIntent pendingIntent) {
+            try {
+                pendingIntent.send(mContext, SmsManager.MMS_ERROR_UNSPECIFIED, null);
+            } catch (PendingIntent.CanceledException e) {
+                Slog.e(TAG, "Failed to return pending intent result", e);
+            }
+        }
+    };
+
     public MmsServiceBroker(Context context) {
         super(context);
         mContext = context;
@@ -145,44 +246,51 @@
         }
     }
 
-    private void ensureService() {
+    private IMms getOrConnectService() {
         synchronized (this) {
-            if (mService == null) {
-                // Service is not connected. Try blocking connecting.
-                Slog.w(TAG, "MmsService not connected. Try connecting...");
-                mConnectionHandler.sendMessage(
-                        mConnectionHandler.obtainMessage(MSG_TRY_CONNECTING));
-                final long shouldEnd =
-                        SystemClock.elapsedRealtime() + SERVICE_CONNECTION_WAIT_TIME_MS;
-                long waitTime = SERVICE_CONNECTION_WAIT_TIME_MS;
-                while (waitTime > 0) {
-                    try {
-                        // TODO: consider using Java concurrent construct instead of raw object wait
-                        this.wait(waitTime);
-                    } catch (InterruptedException e) {
-                        Slog.w(TAG, "Connection wait interrupted", e);
-                    }
-                    if (mService != null) {
-                        // Success
-                        return;
-                    }
-                    // Calculate remaining waiting time to make sure we wait the full timeout period
-                    waitTime = shouldEnd - SystemClock.elapsedRealtime();
-                }
-                // Timed out. Something's really wrong.
-                Slog.e(TAG, "Can not connect to MmsService (timed out)");
-                throw new RuntimeException("Timed out in connecting to MmsService");
+            if (mService != null) {
+                return mService;
             }
+            // Service is not connected. Try blocking connecting.
+            Slog.w(TAG, "MmsService not connected. Try connecting...");
+            mConnectionHandler.sendMessage(
+                    mConnectionHandler.obtainMessage(MSG_TRY_CONNECTING));
+            final long shouldEnd =
+                    SystemClock.elapsedRealtime() + SERVICE_CONNECTION_WAIT_TIME_MS;
+            long waitTime = SERVICE_CONNECTION_WAIT_TIME_MS;
+            while (waitTime > 0) {
+                try {
+                    // TODO: consider using Java concurrent construct instead of raw object wait
+                    this.wait(waitTime);
+                } catch (InterruptedException e) {
+                    Slog.w(TAG, "Connection wait interrupted", e);
+                }
+                if (mService != null) {
+                    // Success
+                    return mService;
+                }
+                // Calculate remaining waiting time to make sure we wait the full timeout period
+                waitTime = shouldEnd - SystemClock.elapsedRealtime();
+            }
+            // Timed out. Something's really wrong.
+            Slog.e(TAG, "Can not connect to MmsService (timed out)");
+            return null;
         }
     }
 
     /**
-     * Making sure when we obtain the mService instance it is always valid.
-     * Throws {@link RuntimeException} when it is empty.
+     * Make sure to return a non-empty service instance. Return the connected MmsService
+     * instance, if not connected, try connecting. If fail to connect, return a fake service
+     * instance which returns failure to service caller.
+     *
+     * @return a non-empty service instance, real or fake
      */
     private IMms getServiceGuarded() {
-        ensureService();
-        return mService;
+        final IMms service = getOrConnectService();
+        if (service != null) {
+            return service;
+        }
+        return mServiceStubForFailure;
     }
 
     private AppOpsManager getAppOpsManager() {
diff --git a/services/core/java/com/android/server/policy/BurnInProtectionHelper.java b/services/core/java/com/android/server/policy/BurnInProtectionHelper.java
index 847eee8..fef1e575 100644
--- a/services/core/java/com/android/server/policy/BurnInProtectionHelper.java
+++ b/services/core/java/com/android/server/policy/BurnInProtectionHelper.java
@@ -27,6 +27,7 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.os.SystemClock;
+import android.util.Slog;
 import android.view.Display;
 import android.view.animation.LinearInterpolator;
 
@@ -45,6 +46,8 @@
     private static final long BURNIN_PROTECTION_WAKEUP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1);
     private static final long BURNIN_PROTECTION_MINIMAL_INTERVAL_MS = TimeUnit.SECONDS.toMillis(10);
 
+    private static final boolean DEBUG = false;
+
     private static final String ACTION_BURN_IN_PROTECTION =
             "android.internal.policy.action.BURN_IN_PROTECTION";
 
@@ -77,10 +80,13 @@
     private BroadcastReceiver mBurnInProtectionReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            if (DEBUG) {
+                Slog.d(TAG, "onReceive " + intent);
+            }
             updateBurnInProtection();
         }
     };
-    
+
     public BurnInProtectionHelper(Context context, int minHorizontalOffset,
             int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset,
             int maxOffsetRadius) {
@@ -136,12 +142,26 @@
                 mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(),
                         mLastBurnInXOffset, mLastBurnInYOffset);
             }
+            // We use currentTimeMillis to compute the next wakeup time since we want to wake up at
+            // the same time as we wake up to update ambient mode to minimize power consumption.
+            // However, we use elapsedRealtime to schedule the alarm so that setting the time can't
+            // disable burn-in protection for extended periods.
+            final long nowWall = System.currentTimeMillis();
+            final long nowElapsed = SystemClock.elapsedRealtime();
             // Next adjustment at least ten seconds in the future.
-            long next = SystemClock.elapsedRealtime() + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS;
+            long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS;
             // And aligned to the minute.
-            next = next - next % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS
+            nextWall = nextWall - nextWall % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS
                     + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS;
-            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mBurnInProtectionIntent);
+            // Use elapsed real time that is adjusted to full minute on wall clock.
+            final long nextElapsed = nowElapsed + (nextWall - nowWall);
+            if (DEBUG) {
+                Slog.d(TAG, "scheduling next wake-up, now wall time " + nowWall
+                        + ", next wall: " + nextWall + ", now elapsed: " + nowElapsed
+                        + ", next elapsed: " + nextElapsed);
+            }
+            mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextElapsed,
+                    mBurnInProtectionIntent);
         } else {
             mAlarmManager.cancel(mBurnInProtectionIntent);
             mCenteringAnimator.start();
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 6bc6de63..d8bb0d7 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3982,7 +3982,10 @@
      */
     public boolean canChangeDtmfToneLength() {
         try {
-            return getITelephony().canChangeDtmfToneLength();
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.canChangeDtmfToneLength();
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "Error calling ITelephony#canChangeDtmfToneLength", e);
         }
@@ -3996,7 +3999,10 @@
      */
     public boolean isWorldPhone() {
         try {
-            return getITelephony().isWorldPhone();
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.isWorldPhone();
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "Error calling ITelephony#isWorldPhone", e);
         }
@@ -4010,7 +4016,10 @@
      */
     public boolean isTtyModeSupported() {
         try {
-            return getITelephony().isTtyModeSupported();
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.isTtyModeSupported();
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "Error calling ITelephony#isTtyModeSupported", e);
         }
@@ -4025,7 +4034,10 @@
      */
     public boolean isHearingAidCompatibilitySupported() {
         try {
-            return getITelephony().isHearingAidCompatibilitySupported();
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.isHearingAidCompatibilitySupported();
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "Error calling ITelephony#isHearingAidCompatibilitySupported", e);
         }
diff --git a/tools/aapt/Resource.cpp b/tools/aapt/Resource.cpp
index 36299c2..38d10cf 100644
--- a/tools/aapt/Resource.cpp
+++ b/tools/aapt/Resource.cpp
@@ -22,7 +22,7 @@
 
 // STATUST: mingw does seem to redefine UNKNOWN_ERROR from our enum value, so a cast is necessary.
 
-#if HAVE_PRINTF_ZD
+#if !defined(_WIN32)
 #  define ZD "%zd"
 #  define ZD_TYPE ssize_t
 #  define STATUST(x) x
diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp
index 941a288..24f8168 100644
--- a/tools/aapt/ResourceTable.cpp
+++ b/tools/aapt/ResourceTable.cpp
@@ -20,7 +20,7 @@
 
 // SSIZE: mingw does not have signed size_t == ssize_t.
 // STATUST: mingw does seem to redefine UNKNOWN_ERROR from our enum value, so a cast is necessary.
-#if HAVE_PRINTF_ZD
+#if !defined(_WIN32)
 #  define SSIZE(x) x
 #  define STATUST(x) x
 #else
diff --git a/tools/aapt/StringPool.cpp b/tools/aapt/StringPool.cpp
index a18e9f1..9908c44 100644
--- a/tools/aapt/StringPool.cpp
+++ b/tools/aapt/StringPool.cpp
@@ -13,7 +13,7 @@
 #include "ResourceTable.h"
 
 // SSIZE: mingw does not have signed size_t == ssize_t.
-#if HAVE_PRINTF_ZD
+#if !defined(_WIN32)
 #  define ZD "%zd"
 #  define ZD_TYPE ssize_t
 #  define SSIZE(x) x
diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp
index d2cd2d6..6902a30 100644
--- a/tools/aapt/XMLNode.cpp
+++ b/tools/aapt/XMLNode.cpp
@@ -18,7 +18,7 @@
 
 // SSIZE: mingw does not have signed size_t == ssize_t.
 // STATUST: mingw does seem to redefine UNKNOWN_ERROR from our enum value, so a cast is necessary.
-#if HAVE_PRINTF_ZD
+#if !defined(_WIN32)
 #  define SSIZE(x) x
 #  define STATUST(x) x
 #else