android/apps: Add CanvasProof App;

Compare Ganesh and HWUI canvas rendering of SKPs on android.

Put SKP files in .../canvasproof/src/main/assets/skps

Run on a Marshmallow device.

NOTREECHECKS=true

Review URL: https://codereview.chromium.org/1258123004
diff --git a/.gitignore b/.gitignore
index 7ea8256..f0e3e29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,7 +21,7 @@
 platform_tools/android/apps/*/src/main/libs
 platform_tools/chromeos/third_party/externals
 platform_tools/chromeos/toolchain
-skps
+/skps
 third_party/externals
 tools/skp/page_sets/data/*.json
 tools/skp/page_sets/data/*.wpr
diff --git a/platform_tools/android/apps/canvasproof/build.gradle b/platform_tools/android/apps/canvasproof/build.gradle
new file mode 100644
index 0000000..64463e8
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+apply plugin: 'com.android.application'
+android {
+    compileSdkVersion 19
+    buildToolsVersion "22.0.1"
+    defaultConfig {
+        applicationId "org.skia.canvasproof"
+        minSdkVersion 9
+        targetSdkVersion 19
+        versionCode 1
+        versionName "1.0"
+        signingConfig signingConfigs.debug
+    }
+    sourceSets.main.jni.srcDirs = [] //disable automatic ndk-build call
+    sourceSets.main.jniLibs.srcDir "src/main/libs"
+    productFlavors { arm {}; arm64 {}; x86 {}; x86_64 {}; mips {}; mips64 {}; }
+    applicationVariants.all{ variant ->
+        def buildNativeLib = task("${variant.name}_NativeLib", type:Exec) {
+            workingDir '../../../..' // top-level skia directory
+            commandLine constructBuildCommand(variant, "CopyCanvasProofDeps").split()
+            environment PATH: getPathWithDepotTools()
+            environment ANDROID_SDK_ROOT: getSDKPath()
+        }
+        buildNativeLib.onlyIf { !project.hasProperty("suppressNativeBuild") }
+        TaskCollection<Task> assembleTask
+        assembleTask = project.tasks.matching {
+            it.name.contains("assemble") &&
+                    it.name.toLowerCase().endsWith(variant.name.toLowerCase())
+        }
+        assembleTask.getAt(0).dependsOn buildNativeLib
+    }
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/AndroidManifest.xml b/platform_tools/android/apps/canvasproof/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..be774c8
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2015 Google Inc.
+
+  Use of this source code is governed by a BSD-style license that can be
+  found in the LICENSE file.
+-->
+<!-- BEGIN_INCLUDE(manifest) -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="org.skia.canvasproof"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="9" />
+    <uses-feature android:glEsVersion="0x00020000" />
+    <application android:label="Canvas Proof">
+        <activity android:name="org.skia.canvasproof.CanvasProofActivity"
+                  android:label="Canvas Proof"
+                  android:configChanges="orientation|keyboardHidden"
+                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+            <meta-data android:name="android.app.lib_name"
+                       android:value="canvasproof" />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
+<!-- END_INCLUDE(manifest) -->
diff --git a/platform_tools/android/apps/canvasproof/src/main/assets/skps/.gitignore b/platform_tools/android/apps/canvasproof/src/main/assets/skps/.gitignore
new file mode 100644
index 0000000..4a70801
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/assets/skps/.gitignore
@@ -0,0 +1 @@
+*.skp
diff --git a/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/CanvasProofActivity.java b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/CanvasProofActivity.java
new file mode 100644
index 0000000..9363585
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/CanvasProofActivity.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package org.skia.canvasproof;
+
+import android.app.Activity;
+import android.content.res.AssetManager;
+import android.graphics.Picture;
+import android.opengl.GLSurfaceView;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.LinearLayout;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class CanvasProofActivity extends Activity {
+    private static final String TAG = "CanvasProofActivity";
+    private GaneshPictureRenderer ganeshPictureRenderer;
+    private HwuiPictureView hwuiPictureView;
+    private GLSurfaceView ganeshPictureView;
+    private LinearLayout splitPaneView;
+    private View currentView;
+    private float x, y;
+    private int resourcesIndex;
+    private class PictureAsset {
+        public String path;
+        public long ptr;
+        public Picture picture;
+    };
+    private PictureAsset[] assets;
+
+    @SuppressWarnings("deprecation")  // purposely using this
+    private static Picture ReadPicture(InputStream inputStream)
+        throws IOException {
+        Picture p = null;
+        try {
+            p = Picture.createFromStream(inputStream);
+        } catch (java.lang.Exception e) {
+            Log.e(TAG, "Exception in Picture.createFromStream", e);
+        }
+        inputStream.close();
+        return p;
+    }
+
+    private void getAssetPaths() {
+        String directory = "skps";
+        AssetManager mgr = this.getAssets();
+        assert (mgr != null);
+        String[] resources;
+        try {
+            resources = mgr.list(directory);
+        } catch (IOException e) {
+            Log.e(TAG, "IOException in getAssetPaths", e);
+            return;
+        }
+        if (resources == null || resources.length == 0) {
+            Log.e(TAG, "SKP assets should be packaged in " +
+                  ".../apps/canvasproof/src/main/assets/skps/" +
+                  ", but none were found.");
+            return;
+        }
+        CreateSkiaPicture.init();
+        this.assets = new PictureAsset[resources.length];
+        for (int i = 0; i < resources.length; ++i) {
+            String path = directory + File.separator + resources[i];
+            this.assets[i] = new PictureAsset();
+            this.assets[i].path = path;
+            try {
+                this.assets[i].ptr = CreateSkiaPicture.create(mgr.open(path));
+                if (0 == this.assets[i].ptr) {
+                    Log.e(TAG, "CreateSkiaPicture.create returned 0 " + path);
+                }
+                Picture p = CanvasProofActivity.ReadPicture(mgr.open(path));
+                if (null == p) {
+                    Log.e(TAG, "CanvasProofActivity.ReadPicture.create " +
+                          "returned null " + path);
+                } else if (0 == p.getHeight() || 0 == p.getWidth()) {
+                    Log.e(TAG, "CanvasProofActivity.ReadPicture.create " +
+                          "empty picture" + path);
+                    p = null;
+                }
+                this.assets[i].picture = p;
+            } catch (IOException e) {
+                Log.e(TAG, "IOException in getAssetPaths " + path + e);
+                return;
+            }
+        }
+    }
+
+    private void nextView() {
+        if (this.currentView == this.hwuiPictureView) {
+            this.currentView = this.ganeshPictureView;
+            this.ganeshPictureView.setRenderMode(
+                    GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+        } else if (this.currentView == null ||
+                   this.currentView == this.ganeshPictureView) {
+            this.setContentView(new View(this));
+            LayoutParams layoutParams =
+                new LayoutParams(
+                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 0.5f);
+            this.ganeshPictureView.setLayoutParams(layoutParams);
+            this.ganeshPictureView.setRenderMode(
+                    GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+            this.splitPaneView.addView(this.ganeshPictureView);
+            this.hwuiPictureView.setLayoutParams(layoutParams);
+            this.splitPaneView.addView(this.hwuiPictureView);
+            this.currentView = this.splitPaneView;
+            this.hwuiPictureView.fullTime = false;
+        } else if (this.currentView == this.splitPaneView) {
+            this.splitPaneView.removeAllViews();
+            this.currentView = this.hwuiPictureView;
+            this.hwuiPictureView.fullTime = true;
+        } else {
+            Log.e(TAG, "unexpected value");
+            this.setContentView(null);
+            return;
+        }
+        this.setContentView(this.currentView);
+        this.currentView.invalidate();
+    }
+
+    private void nextPicture(int d) {
+        if (this.assets == null) {
+            Log.w(TAG, "this.assets == null");
+            return;
+        }
+        assert (this.assets.length > 0);
+        resourcesIndex = (resourcesIndex + d) % this.assets.length;
+        while (resourcesIndex < 0) {
+            resourcesIndex += this.assets.length;
+        }
+        while (resourcesIndex >= this.assets.length) {
+            resourcesIndex -= this.assets.length;
+        }
+        this.ganeshPictureRenderer.setPicture(assets[resourcesIndex].ptr);
+        this.hwuiPictureView.setPicture(assets[resourcesIndex].picture);
+        this.currentView.invalidate();
+    }
+
+    @Override
+    protected void onStop() {
+        this.ganeshPictureRenderer.releaseResources();
+        super.onStop();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        this.getAssetPaths();
+        this.ganeshPictureRenderer = new GaneshPictureRenderer();
+        this.hwuiPictureView = new HwuiPictureView(this);
+
+        this.ganeshPictureRenderer.setScale(2.0f);
+        this.hwuiPictureView.setScale(2.0f);
+        this.ganeshPictureView = ganeshPictureRenderer.makeView(this);
+        this.splitPaneView = new LinearLayout(this);
+        this.splitPaneView.setOrientation(LinearLayout.VERTICAL);
+        this.splitPaneView.setGravity(Gravity.FILL);
+
+        LayoutParams layoutParams =
+            new LayoutParams(
+                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 0.5f);
+        this.ganeshPictureView.setLayoutParams(layoutParams);
+        this.hwuiPictureView.setLayoutParams(layoutParams);
+
+        this.nextView();
+        this.nextPicture(0);
+    }
+
+    // TODO: replace this funtion with onTouchEvent().
+    // @Override public boolean onTouchEvent(MotionEvent event)...
+    @Override
+    public boolean dispatchTouchEvent (MotionEvent event) {
+        switch(event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                this.x = event.getX();
+                this.y = event.getY();
+                break;
+            case MotionEvent.ACTION_UP:
+                float dx = event.getX() - this.x;
+                float dy = event.getY() - this.y;
+                float dx2 = dx * dx;
+                float dy2 = dy * dy;
+                if (dx2 + dy2 > 22500.0) {
+                    if (dy2 < dx2) {
+                        this.nextPicture(dx > 0 ? 1 : -1);
+                    } else if (dy > 0) {
+                        this.nextView();
+                    }
+                }
+                break;
+        }
+        return super.onTouchEvent(event);
+    }
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/CreateSkiaPicture.java b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/CreateSkiaPicture.java
new file mode 100644
index 0000000..61aa14a
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/CreateSkiaPicture.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+/*
+AJAR=$ANDROID_SDK_ROOT/platforms/android-19/android.jar
+CLASS=CreateSkiaPicture
+SRC=platform_tools/android/apps/canvasproof/src/main
+javac -classpath $AJAR $SRC/java/org/skia/canvasproof/$CLASS.java
+javah -classpath $AJAR:$SRC/java -d $SRC/jni org.skia.canvasproof.$CLASS
+*/
+
+package org.skia.canvasproof;
+
+import android.util.Log;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.UnsatisfiedLinkError;
+
+public class CreateSkiaPicture {
+    private static final String TAG = "CreateSkiaPicture";
+
+    public static void init() {
+        try {
+            System.loadLibrary("skia_android");
+            System.loadLibrary("canvasproof");
+        } catch (java.lang.Error e) {
+            Log.v(TAG, "System.loadLibrary error", e);
+        }
+    }
+
+    public static long create(InputStream inputStream) throws IOException {
+        byte[] buffer = new byte[16 * (1 << 10)];  // 16 KByte
+        long p = 0;
+        try {
+            p = CreateSkiaPicture.createImpl(inputStream, buffer);
+        } catch (UnsatisfiedLinkError e) {
+            Log.e(TAG, "UnsatisfiedLinkError createImpl");
+        }
+        inputStream.close();
+        return p;
+    }
+
+    public static void delete(long ptr) {
+        try {
+            if (ptr != 0) {
+                CreateSkiaPicture.deleteImpl(ptr);
+            }
+        } catch (UnsatisfiedLinkError e) {
+            Log.e(TAG, "UnsatisfiedLinkError deleteImpl");
+        }
+
+    }
+    private static native void deleteImpl(long ptr);
+    private static native long createImpl(InputStream s, byte[] b);
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/GaneshPictureRenderer.java b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/GaneshPictureRenderer.java
new file mode 100644
index 0000000..01c6dc3
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/GaneshPictureRenderer.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+// AJAR=$ANDROID_SDK_ROOT/platforms/android-19/android.jar
+// SRC=platform_tools/android/apps/canvasproof/src/main
+// javac -classpath $AJAR $SRC/java/org/skia/canvasproof/GaneshPictureRenderer.java
+// javah -classpath $AJAR:$SRC/java -d $SRC/jni org.skia.canvasproof.GaneshPictureRenderer
+
+package org.skia.canvasproof;
+
+import android.app.Activity;
+import android.graphics.Rect;
+import android.opengl.GLSurfaceView;
+import android.util.Log;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+public class GaneshPictureRenderer implements GLSurfaceView.Renderer {
+    private static final String TAG = "GaneshPictureRenderer";
+    private long picturePtr;
+    private long contextPtr;
+    private float scale;
+    private int width;
+    private int height;
+    private GLSurfaceView view;
+
+    GaneshPictureRenderer() {
+        try {
+            System.loadLibrary("skia_android");
+            System.loadLibrary("canvasproof");
+        } catch (java.lang.Error e) {
+            Log.e(TAG, "System.loadLibrary error", e);
+            return;
+        }
+        this.scale = 1;
+    }
+    public GLSurfaceView makeView(Activity activity) {
+        this.view = new GLSurfaceView(activity);
+        this.view.setEGLConfigChooser(8, 8, 8, 8, 0, 8);
+        this.view.setEGLContextClientVersion(2);
+        this.view.setRenderer(this);
+        this.view.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+        return this.view;
+    }
+    static public Rect cullRect(long picturePtr) {
+        Rect rect = new Rect();
+        try {
+            GaneshPictureRenderer.GetCullRect(rect, picturePtr);
+        } catch (UnsatisfiedLinkError e) {
+            Log.e(TAG, "GetCullRect failed", e);
+        }
+        return rect;
+    }
+    public void setPicture(long picturePtr) {
+        this.picturePtr = picturePtr;
+        this.view.requestRender();
+    }
+    public void setScale(float s) { this.scale = s; }
+
+    public void releaseResources() {
+        if (this.contextPtr != 0) {
+            try {
+                GaneshPictureRenderer.CleanUp(this.contextPtr);
+            } catch (UnsatisfiedLinkError e) {
+                Log.e(TAG, "CleanUp failed", e);
+            }
+        }
+        this.contextPtr = 0;
+    }
+
+    private void createContext() {
+        try {
+            this.contextPtr = GaneshPictureRenderer.Ctor();
+        } catch (UnsatisfiedLinkError e) {
+            Log.e(TAG, "Ctor failed", e);
+        }
+    }
+
+    @Override
+    public void onSurfaceCreated(GL10 gl, EGLConfig c) {
+        this.releaseResources();
+        this.createContext();
+    }
+    @Override
+    public void onDrawFrame(GL10 gl) {
+        if (this.contextPtr == 0) {
+            this.createContext();
+        }
+        if (this.width > 0 && this.height > 0 &&
+            this.contextPtr != 0 && this.picturePtr != 0) {
+            try {
+                GaneshPictureRenderer.DrawThisFrame(
+                        this.width, this.height, this.scale,
+                        this.contextPtr, this.picturePtr);
+            } catch (UnsatisfiedLinkError e) {
+                Log.e(TAG, "DrawThisFrame failed", e);
+            }
+        }
+    }
+    @Override
+    public void onSurfaceChanged(GL10 gl, int w, int h) {
+        this.width = w;
+        this.height = h;
+    }
+    @Override
+    public void finalize() throws Throwable {
+        super.finalize();
+        this.releaseResources();
+    }
+
+    // Make the native functions static to simplify JNI interaction.
+    private static native void DrawThisFrame(int w, int h, float s, long pr, long pc);
+    private static native long Ctor();
+    private static native void CleanUp(long p);
+    private static native void GetCullRect(Rect r, long picture);
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/HwuiPictureView.java b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/HwuiPictureView.java
new file mode 100644
index 0000000..872089c
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/java/org/skia/canvasproof/HwuiPictureView.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package org.skia.canvasproof;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Picture;
+import android.util.Log;
+import android.view.View;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class HwuiPictureView extends View {
+    private static final String TAG = "HwuiPictureView";
+    private Picture picture;
+    private float scale;
+    private Picture defaultPicture;
+
+    public boolean fullTime;
+
+    HwuiPictureView(Context context) {
+        super(context);
+        this.scale = 1.0f;
+    }
+    public void setScale(float s) {
+        this.scale = s;
+    }
+    public void setPicture(Picture p) {
+        this.picture = p;
+        this.invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (this.picture != null) {
+            canvas.save();
+            canvas.scale(scale, scale);
+            HwuiPictureView.draw(canvas, this.picture);
+            canvas.restore();
+            if (fullTime) {
+                this.invalidate();
+            }
+        }
+    }
+
+    static private void draw(Canvas canvas, Picture p) {
+        if (android.os.Build.VERSION.SDK_INT > 22) {
+            try {
+                canvas.drawPicture(p);
+                return;
+            } catch (java.lang.Exception e) {
+                Log.e(TAG, "Exception while drawing picture in Hwui");
+            }
+        }
+        if (p.getWidth() > 0 && p.getHeight() > 0) {
+            // Fallback to software rendering.
+            Bitmap bm = Bitmap.createBitmap(p.getWidth(), p.getHeight(),
+                                            Bitmap.Config.ARGB_8888);
+            (new Canvas(bm)).drawPicture(p);
+            canvas.drawBitmap(bm, 0.0f, 0.0f, null);
+            bm.recycle();
+        }
+    }
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/jni/JavaInputStream.cpp b/platform_tools/android/apps/canvasproof/src/main/jni/JavaInputStream.cpp
new file mode 100644
index 0000000..823b72f
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/jni/JavaInputStream.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "JavaInputStream.h"
+
+JavaInputStream::JavaInputStream(
+        JNIEnv* env, jbyteArray javaBuffer, jobject inputStream)
+    : fEnv(env)
+    , fStartIndex(0)
+    , fEndIndex(0) {
+    SkASSERT(inputStream);
+    SkASSERT(javaBuffer);
+    fInputStream = inputStream;
+    fJavaBuffer = javaBuffer;
+    fInputStreamClass = env->FindClass("java/io/InputStream");
+    SkASSERT(fInputStreamClass);
+    fReadMethodID = env->GetMethodID(fInputStreamClass, "read", "([B)I");
+    SkASSERT(fReadMethodID);
+}
+
+bool JavaInputStream::isAtEnd() const { return fStartIndex == fEndIndex; }
+
+size_t JavaInputStream::read(void* voidBuffer, size_t size) {
+    size_t totalRead = 0;
+    char* buffer = static_cast<char*>(voidBuffer);  // may be NULL;
+    while (size) {
+        // make sure the cache has at least one byte or is done.
+        if (fStartIndex == fEndIndex) {
+            jint count =
+                fEnv->CallIntMethod(fInputStream, fReadMethodID, fJavaBuffer);
+            if (fEnv->ExceptionCheck()) {
+                fEnv->ExceptionDescribe();
+                fEnv->ExceptionClear();
+                SkDebugf("---- java.io.InputStream::read() threw an exception\n");
+                return 0;
+            }
+            fStartIndex = 0;
+            fEndIndex = count;
+            if (this->isAtEnd()) {
+                return totalRead;  // No more to read.
+            }
+        }
+        SkASSERT(fEndIndex > fStartIndex);
+        size_t length = SkTMin(SkToSizeT(fEndIndex - fStartIndex), size);
+        if (buffer && length) {
+            jbyte* bufferElements
+                = fEnv->GetByteArrayElements(fJavaBuffer, NULL);
+            memcpy(buffer, &bufferElements[fStartIndex], length);
+            buffer += length;
+            fEnv->ReleaseByteArrayElements(fJavaBuffer, bufferElements, 0);
+        }
+        totalRead += length;
+        size -= length;
+        fStartIndex += length;
+    }
+    return totalRead;
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/jni/JavaInputStream.h b/platform_tools/android/apps/canvasproof/src/main/jni/JavaInputStream.h
new file mode 100644
index 0000000..e14e026
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/jni/JavaInputStream.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef JavaInputStream_DEFINED
+#define JavaInputStream_DEFINED
+
+#include <jni.h>
+#include "SkStream.h"
+
+class JavaInputStream : public SkStream {
+public:
+    JavaInputStream(JNIEnv*, jbyteArray javaBuffer, jobject javaIoInputStream);
+    bool isAtEnd() const override;
+    size_t read(void*, size_t) override;
+private:
+    JNIEnv* fEnv;
+    jobject fInputStream;
+    jbyteArray fJavaBuffer;
+    jclass fInputStreamClass;
+    jmethodID fReadMethodID;
+    jint fStartIndex;
+    jint fEndIndex;
+};
+
+#endif  // JavaInputStream_DEFINED
diff --git a/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.cpp b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.cpp
new file mode 100644
index 0000000..c5d9759
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.cpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "org_skia_canvasproof_CreateSkiaPicture.h"
+#include "JavaInputStream.h"
+#include "SkPicture.h"
+#include "SkPictureRecorder.h"
+
+/*
+ * Class:     org_skia_canvasproof_CreateSkiaPicture
+ * Method:    delete
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_CreateSkiaPicture_deleteImpl(
+        JNIEnv* env, jclass clazz, jlong ptr) {
+    SkSafeUnref(reinterpret_cast<SkPicture*>(ptr));
+}
+
+/*
+ * Class:     org_skia_canvasproof_CreateSkiaPicture
+ * Method:    createImpl
+ * Signature: (Ljava/io/InputStream;[B)J
+ */
+JNIEXPORT jlong JNICALL Java_org_skia_canvasproof_CreateSkiaPicture_createImpl
+  (JNIEnv* env, jclass clazz, jobject inputStream, jbyteArray buffer) {
+    JavaInputStream stream(env, buffer, inputStream);
+    #if 0
+    SkAutoTUnref<SkPicture> p(SkPicture::CreateFromStream(&stream));
+    if (!p) { return 0; }
+    SkPictureRecorder recorder;
+    SkRect bounds = p->cullRect();
+    SkRTreeFactory bbh;
+    recorder.beginRecording(bounds, &bbh)->drawPicture(p);
+    return reinterpret_cast<long>(recorder.endRecordingAsPicture());
+    #else
+    return reinterpret_cast<long>(SkPicture::CreateFromStream(&stream));
+    #endif
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.h b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.h
new file mode 100644
index 0000000..2937d54
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class org_skia_canvasproof_CreateSkiaPicture */
+
+#ifndef _Included_org_skia_canvasproof_CreateSkiaPicture
+#define _Included_org_skia_canvasproof_CreateSkiaPicture
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     org_skia_canvasproof_CreateSkiaPicture
+ * Method:    deleteImpl
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_CreateSkiaPicture_deleteImpl
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_skia_canvasproof_CreateSkiaPicture
+ * Method:    createImpl
+ * Signature: (Ljava/io/InputStream;[B)J
+ */
+JNIEXPORT jlong JNICALL Java_org_skia_canvasproof_CreateSkiaPicture_createImpl
+  (JNIEnv *, jclass, jobject, jbyteArray);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.cpp b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.cpp
new file mode 100644
index 0000000..d1de529
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.cpp
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "org_skia_canvasproof_GaneshPictureRenderer.h"
+
+#include "GrContext.h"
+#include "JavaInputStream.h"
+#include "SkCanvas.h"
+#include "SkMatrix.h"
+#include "SkPicture.h"
+#include "SkRect.h"
+#include "SkStream.h"
+#include "SkSurface.h"
+
+#define TAG "GaneshPictureRenderer.cpp: "
+
+static void render_picture(GrContext* grContext,
+                           int width,
+                           int height,
+                           const SkPicture* picture,
+                           const SkMatrix& matrix) {
+    SkASSERT(grContext);
+    if (!picture) {
+        SkDebugf(TAG "!picture\n");
+        return;
+    }
+    // Render to the default framebuffer render target.
+    GrBackendRenderTargetDesc desc;
+    desc.fWidth = width;
+    desc.fHeight = height;
+    desc.fConfig = kSkia8888_GrPixelConfig;
+    desc.fOrigin = kBottomLeft_GrSurfaceOrigin;
+    SkSurfaceProps surfaceProps(SkSurfaceProps::kUseDeviceIndependentFonts_Flag,
+                                kUnknown_SkPixelGeometry);
+    // TODO:  Check to see if we can keep the surface between draw calls.
+    SkAutoTUnref<SkSurface> surface(
+            SkSurface::NewFromBackendRenderTarget(
+                    grContext, desc, &surfaceProps));
+    if (surface) {
+        SkCanvas* canvas = surface->getCanvas();
+        SkASSERT(canvas);
+        canvas->clear(SK_ColorGRAY);
+        canvas->concat(matrix);
+        SkRect cullRect = picture->cullRect();
+        canvas->clipRect(cullRect);
+        picture->playback(canvas);
+        canvas->flush();
+    }
+}
+
+namespace {
+struct GaneshPictureRendererImpl {
+    SkAutoTUnref<GrContext> fGrContext;
+    void render(int w, int h, const SkPicture* p, const SkMatrix& m) {
+        if (!fGrContext) {
+            // Cache the rendering context between frames.
+            fGrContext.reset(GrContext::Create(kOpenGL_GrBackend, 0));
+            if (!fGrContext) {
+                SkDebugf(TAG "GrContext::Create - failed\n");
+                return;
+            }
+        }
+        render_picture(fGrContext, w, h, p, m);
+    }
+};
+}  // namespace
+
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    DrawThisFrame
+ * Signature: (IIFJ)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_DrawThisFrame(
+        JNIEnv*, jclass, jint width, jint height, jfloat scale, jlong ptr, jlong pic) {
+    if (!ptr) { return; }
+    SkMatrix matrix = SkMatrix::MakeScale((SkScalar)scale);
+    GaneshPictureRendererImpl* impl =
+        reinterpret_cast<GaneshPictureRendererImpl*>(ptr);
+    SkPicture* picture = reinterpret_cast<SkPicture*>(pic);
+    impl->render((int)width, (int)height, picture, matrix);
+}
+
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    Ctor
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_Ctor
+  (JNIEnv *, jclass) {
+    return reinterpret_cast<jlong>(new GaneshPictureRendererImpl);
+}
+
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    CleanUp
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_CleanUp
+  (JNIEnv *, jclass, jlong ptr) {
+    delete reinterpret_cast<GaneshPictureRendererImpl*>(ptr);
+}
+
+namespace {
+struct AndroidRectHelper {
+    jfieldID fLeft, fTop, fRight, fBottom;
+    AndroidRectHelper()
+        : fLeft(nullptr), fTop(nullptr), fRight(nullptr), fBottom(nullptr) {}
+    void config(JNIEnv *env) {
+        if (!fLeft) {
+            jclass rectClass = env->FindClass("android/graphics/Rect");
+            SkASSERT(rectClass);
+            fLeft = env->GetFieldID(rectClass, "left", "I");
+            fTop = env->GetFieldID(rectClass, "top", "I");
+            fRight = env->GetFieldID(rectClass, "right", "I");
+            fBottom = env->GetFieldID(rectClass, "bottom", "I");
+        }
+    }
+};
+} // namespace
+
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    GetCullRect
+ * Signature: (Landroid/graphics/Rect;J)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_GetCullRect
+  (JNIEnv *env, jclass, jobject androidGraphicsRect, jlong picturePtr) {
+    SkASSERT(androidGraphicsRect);
+    const SkPicture* picture = reinterpret_cast<SkPicture*>(picturePtr);
+    SkRect rect = SkRect::MakeEmpty();
+    if (picture) {
+        rect = picture->cullRect();
+    }
+    SkIRect iRect;
+    rect.roundOut(&iRect);
+    static AndroidRectHelper help;
+    help.config(env);
+    env->SetIntField(androidGraphicsRect, help.fLeft, (jint)(iRect.left()));
+    env->SetIntField(androidGraphicsRect, help.fTop, (jint)(iRect.top()));
+    env->SetIntField(androidGraphicsRect, help.fRight, (jint)(iRect.right()));
+    env->SetIntField(androidGraphicsRect, help.fBottom, (jint)(iRect.bottom()));
+}
diff --git a/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.h b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.h
new file mode 100644
index 0000000..401fa87
--- /dev/null
+++ b/platform_tools/android/apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class org_skia_canvasproof_GaneshPictureRenderer */
+
+#ifndef _Included_org_skia_canvasproof_GaneshPictureRenderer
+#define _Included_org_skia_canvasproof_GaneshPictureRenderer
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    DrawThisFrame
+ * Signature: (IIFJJ)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_DrawThisFrame
+  (JNIEnv *, jclass, jint, jint, jfloat, jlong, jlong);
+
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    Ctor
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_Ctor
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    CleanUp
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_CleanUp
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_skia_canvasproof_GaneshPictureRenderer
+ * Method:    GetCullRect
+ * Signature: (Landroid/graphics/Rect;J)V
+ */
+JNIEXPORT void JNICALL Java_org_skia_canvasproof_GaneshPictureRenderer_GetCullRect
+  (JNIEnv *, jclass, jobject, jlong);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/platform_tools/android/apps/settings.gradle b/platform_tools/android/apps/settings.gradle
index 89cc954..787bcf0 100644
--- a/platform_tools/android/apps/settings.gradle
+++ b/platform_tools/android/apps/settings.gradle
@@ -1,2 +1,3 @@
 include ':sample_app'
 include ':visualbench'
+include ':canvasproof'
diff --git a/platform_tools/android/gyp/canvasproof.gypi b/platform_tools/android/gyp/canvasproof.gypi
new file mode 100644
index 0000000..f733d0e
--- /dev/null
+++ b/platform_tools/android/gyp/canvasproof.gypi
@@ -0,0 +1,75 @@
+# Copyright 2015 Google Inc.
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+{
+  'targets': [
+    {
+      'target_name': 'canvasproof',
+      'type': 'shared_library',
+      'dependencies': [ 'skia_lib.gyp:skia_lib', ],
+      'sources': [
+        '../apps/canvasproof/src/main/jni/JavaInputStream.cpp',
+        '../apps/canvasproof/src/main/jni/JavaInputStream.h',
+        '../apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.cpp',
+        '../apps/canvasproof/src/main/jni/org_skia_canvasproof_GaneshPictureRenderer.h',
+        '../apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.cpp',
+        '../apps/canvasproof/src/main/jni/org_skia_canvasproof_CreateSkiaPicture.h  ',
+      ],
+    },
+    {
+      'target_name': 'CopyCanvasProofDeps',
+      'type': 'none',
+      'dependencies': [
+        'skia_lib.gyp:skia_lib',
+        'canvasproof',
+      ],
+      'copies': [
+        {
+          'destination': '../apps/canvasproof/src/main/libs/<(android_arch)',
+          'conditions': [
+            [ 'skia_shared_lib', {
+              'files': [
+                '<(SHARED_LIB_DIR)/libskia_android.so',
+                '<(SHARED_LIB_DIR)/libcanvasproof.so',
+              ]}, {
+              'files': [
+                '<(SHARED_LIB_DIR)/libcanvasproof.so',
+              ]}
+           ],
+          ],
+        },
+      ],
+    },
+    {
+      'target_name': 'CanvasProof_APK',
+      'type': 'none',
+      'dependencies': [ 'CopyCanvasProofDeps', ],
+      'actions': [
+        {
+          'action_name': 'SkiaCanvasProof_apk',
+          'inputs': [
+            '../apps/canvasproof/src/main/assets/skps',
+            '../apps/canvasproof/src/main/AndroidManifest.xml',
+            '../apps/canvasproof/src/main/java/org/skia/canvasproof/CreateSkiaPicture.java',
+            '../apps/canvasproof/src/main/java/org/skia/canvasproof/CanvasProofActivity.java',
+            '../apps/canvasproof/src/main/java/org/skia/canvasproof/GaneshPictureRenderer.java',
+            '../apps/canvasproof/src/main/java/org/skia/canvasproof/HwuiPictureView.java',
+            '<(android_base)/apps/canvasproof/src/main/libs/<(android_arch)/libcanvasproof.so',
+            '<(android_base)/apps/canvasproof/src/main/libs/<(android_arch)/libskia_android.so',
+
+          ],
+          'outputs': [
+            '../apps/canvasproof/build',
+          ],
+          'action': [
+            '<(android_base)/apps/gradlew',
+            ':canvasproof:assemble<(android_variant)Debug',
+            '-p<(android_base)/apps/canvasproof',
+            '-PsuppressNativeBuild',
+          ],
+        },
+      ],
+    },
+  ],
+}
diff --git a/platform_tools/android/gyp/skia_android.gypi b/platform_tools/android/gyp/skia_android.gypi
index 92ee5dd..09a63d5 100644
--- a/platform_tools/android/gyp/skia_android.gypi
+++ b/platform_tools/android/gyp/skia_android.gypi
@@ -45,6 +45,7 @@
       }],
     ],
   },
+  'includes' : [ 'canvasproof.gypi', ],
   'targets': [
     {
       'target_name': 'CopySampleAppDeps',