Revert of Remove Android SampleApp (patchset #5 id:80001 of https://codereview.chromium.org/2096683002/ )

Reason for revert:
Testing to see if this CL caused the failures in Build-Mac-Clang-Arm7-Debug-Android

Original issue's description:
> Remove Android SampleApp
>
> BUG=skia:
> GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2096683002
>
> Committed: https://skia.googlesource.com/skia/+/41bb5b40e71411ee39538399ea125986ec760a90

TBR=djsollen@google.com,liyuqian@google.com
# Skipping CQ checks because original CL landed less than 1 days ago.
NOPRESUBMIT=true
NOTREECHECKS=true
NOTRY=true
BUG=skia:

Review-Url: https://codereview.chromium.org/2099693002
diff --git a/gyp/SampleApp.gyp b/gyp/SampleApp.gyp
index beaaeca..347af56 100644
--- a/gyp/SampleApp.gyp
+++ b/gyp/SampleApp.gyp
@@ -114,6 +114,23 @@
             '../experimental/iOSSampleApp/iPhone/MainWindow_iPhone.xib',
           ],
         }],
+        [ 'skia_os == "android"', {
+          'conditions': [
+            ['skia_android_framework == 0', {
+              'dependencies': [
+                'android_deps.gyp:Android_EntryPoint',
+                'skia_launcher.gyp:skia_launcher',
+              ],
+            }],
+          ],
+          'dependencies!': [
+            'experimental.gyp:experimental',
+          ],
+          'dependencies': [
+            'android_output.gyp:android_output',
+            'android_deps.gyp:Android_SampleApp',
+          ],
+        }],
         [ 'skia_gpu == 1', {
           'dependencies': [
             'gputest.gyp:skgputest',
diff --git a/gyp/everything.gyp b/gyp/everything.gyp
index beda91f..02a5474 100644
--- a/gyp/everything.gyp
+++ b/gyp/everything.gyp
@@ -31,7 +31,7 @@
         }],
         ['skia_os == "android"', {
           'dependencies': [
-            'android_system.gyp:Viewer_APK',
+            'android_system.gyp:SampleApp_APK',
           ],
         }],
       ],
diff --git a/gyp/most.gyp b/gyp/most.gyp
index 7070512..eb9f6c0 100644
--- a/gyp/most.gyp
+++ b/gyp/most.gyp
@@ -39,13 +39,10 @@
             'example.gyp:HelloWorld',
           ],
         }],
-        ['skia_os == "android"', {
+        ['skia_os == "android" and skia_vulkan == 1', {
           'dependencies': [
             'android_system.gyp:Viewer_APK',
           ],
-          'dependencies!': [
-            'SampleApp.gyp:SampleApp',
-          ],
         }],
         ['skia_os == "ios"', {
           'dependencies!': [
diff --git a/gyp/tools.gyp b/gyp/tools.gyp
index 68af294..ab8b4c6 100644
--- a/gyp/tools.gyp
+++ b/gyp/tools.gyp
@@ -496,6 +496,7 @@
           '<(skia_include_path)/utils/win',
           '<(skia_include_path)/utils/SkDebugUtils.h',
           '<(skia_include_path)/utils/SkJSONCPP.h',
+          '<(skia_include_path)/views/SkOSWindow_Android.h',
           '<(skia_include_path)/views/SkOSWindow_iOS.h',
           '<(skia_include_path)/views/SkOSWindow_Mac.h',
           '<(skia_include_path)/views/SkOSWindow_SDL.h',
diff --git a/include/views/SkOSWindow_Android.h b/include/views/SkOSWindow_Android.h
new file mode 100644
index 0000000..234e65c
--- /dev/null
+++ b/include/views/SkOSWindow_Android.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2011 Skia
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkOSWindow_Android_DEFINED
+#define SkOSWindow_Android_DEFINED
+
+#include "SkWindow.h"
+
+#include <EGL/egl.h>
+
+struct SkAndroidWindow {
+    EGLDisplay fDisplay;
+    EGLSurface fSurface;
+    EGLContext fContext;
+};
+
+class SkOSWindow : public SkWindow {
+public:
+    SkOSWindow(void*);
+    ~SkOSWindow();
+
+    enum SkBackEndTypes {
+        kNone_BackEndType,
+        kNativeGL_BackEndType,
+    };
+
+    bool attach(SkBackEndTypes attachType, int msaaSampleCount, bool deepColor,
+                AttachmentInfo* info);
+    void release();
+    void present();
+    bool makeFullscreen() { return true; }
+    void closeWindow();
+    void setVsync(bool);
+    bool destroyRequested() { return fDestroyRequested; }
+
+protected:
+    // overrides from SkWindow
+    virtual void onHandleInval(const SkIRect&);
+    virtual void onSetTitle(const char title[]);
+
+private:
+    SkAndroidWindow fWindow;
+    ANativeWindow* fNativeWindow;
+    bool fDestroyRequested;
+
+    typedef SkWindow INHERITED;
+};
+
+#endif
diff --git a/include/views/SkWindow.h b/include/views/SkWindow.h
index f81ec5d..5077484 100644
--- a/include/views/SkWindow.h
+++ b/include/views/SkWindow.h
@@ -129,7 +129,7 @@
 #elif defined(SK_BUILD_FOR_WIN)
     #include "SkOSWindow_Win.h"
 #elif defined(SK_BUILD_FOR_ANDROID)
-    #error Android does not support SkOSWindow and SampleApp. Please use Viewer instead.
+    #include "SkOSWindow_Android.h"
 #elif defined(SK_BUILD_FOR_UNIX)
     #include "SkOSWindow_Unix.h"
 #elif defined(SK_BUILD_FOR_IOS)
diff --git a/platform_tools/android/apps/sample_app/build.gradle b/platform_tools/android/apps/sample_app/build.gradle
new file mode 100644
index 0000000..4678980
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/build.gradle
@@ -0,0 +1,30 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 19
+    buildToolsVersion "22.0.1"
+
+    defaultConfig {
+        applicationId "com.skia.sample_app"
+        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 {}
+    }
+
+    // make sure that app is built and shared libraries are copied to correct directories
+    setupSkiaLibraryBuild(project, applicationVariants, "CopySampleAppDeps")
+}
\ No newline at end of file
diff --git a/platform_tools/android/apps/sample_app/src/main/AndroidManifest.xml b/platform_tools/android/apps/sample_app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..edb0424
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.skia"
+      android:versionCode="1"
+      android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="14" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_ALL_EXTERNAL_STORAGE" />
+    <!-- Needed to add to the download manager. -->
+    <uses-permission android:name="android.permission.INTERNET" />
+    <application android:label="SkiaAndroid">
+        <activity android:name=".SkiaSampleActivity"
+                  android:theme="@android:style/Theme.Holo.Light"
+                  android:configChanges="orientation|screenSize"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleActivity.java b/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleActivity.java
new file mode 100644
index 0000000..6e30696
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleActivity.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package com.skia;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.DownloadManager;
+import android.content.Intent;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.io.File;
+
+public class SkiaSampleActivity extends Activity
+{
+    private TextView mTitle;
+    private SkiaSampleView mSampleView;
+
+    private ArrayAdapter<String> mSlideList;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.layout);
+        mTitle = (TextView) findViewById(R.id.title_view);
+        mSlideList = new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1);
+
+        try {
+            System.loadLibrary("skia_android");
+        } catch (UnsatisfiedLinkError e) {
+            // This might be because skia was linked to SampleApp statically.
+        }
+
+        try {
+            System.loadLibrary("SampleApp");
+
+            createSampleView(false, 0);
+
+            setupActionBar();
+        } catch (UnsatisfiedLinkError e) {
+            mTitle.setText("ERROR: native library could not be loaded");
+        }
+    }
+
+    private void createSampleView(boolean useOpenGLAPI, int msaaSampleCount) {
+        if (mSampleView != null) {
+            ViewGroup viewGroup = (ViewGroup) mSampleView.getParent();
+            viewGroup.removeView(mSampleView);
+            mSampleView.terminate();
+        }
+
+        // intent get intent extras if triggered from the command line
+        Intent intent = this.getIntent();
+        String flags = intent.getStringExtra("cmdLineFlags");
+        
+        if (flags == null || flags.isEmpty()) {
+            flags  = "--pictureDir /data/local/tmp/skia_skp ";
+            flags += "--resourcePath /data/local/tmp/skia_resources ";
+        }
+        
+        mSampleView = new SkiaSampleView(this, flags, useOpenGLAPI, msaaSampleCount);
+        LinearLayout holder = (LinearLayout) findViewById(R.id.holder);
+        holder.addView(mSampleView, new LinearLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT));
+    }
+
+    private void setupActionBar() {
+        ActionBar.OnNavigationListener navigationCallback = new ActionBar.OnNavigationListener() {
+            @Override
+            public boolean onNavigationItemSelected(int position, long itemId) {
+                mSampleView.goToSample(position);
+                return true;
+            }
+        };
+
+        ActionBar actionBar = getActionBar();
+        actionBar.setDisplayShowHomeEnabled(false);
+        actionBar.setDisplayShowTitleEnabled(false);
+        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+        actionBar.setListNavigationCallbacks(mSlideList, navigationCallback);
+    }
+
+    @Override
+    protected void onResume () {
+        super.onResume();
+        if (mSampleView != null && mSampleView.getWidth() > 0 && mSampleView.getHeight() > 0) {
+            //TODO try mSampleView.requestRender() instead
+            mSampleView.inval();
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        if (mSampleView != null) {
+            mSampleView.terminate();
+        }
+        super.onDestroy();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.action_bar, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        if (mSampleView != null) {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                ((MenuItem) menu.findItem(R.id.glcontext_menu))
+                    .setEnabled(false);
+
+            } else {
+                boolean usesOpenGLAPI = mSampleView.getUsesOpenGLAPI();
+                boolean isMSAA4 = mSampleView.getMSAASampleCount() == 4;
+
+                ((MenuItem) menu.findItem(R.id.glcontext_opengles))
+                    .setChecked(!usesOpenGLAPI && !isMSAA4);
+
+                ((MenuItem) menu.findItem(R.id.glcontext_msaa4_opengles))
+                    .setChecked(!usesOpenGLAPI && isMSAA4);
+
+                ((MenuItem) menu.findItem(R.id.glcontext_opengl))
+                    .setChecked(usesOpenGLAPI && !isMSAA4);
+
+                ((MenuItem) menu.findItem(R.id.glcontext_msaa4_opengl))
+                    .setChecked(usesOpenGLAPI && isMSAA4);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (mSampleView == null) {
+            return false;
+        }
+
+        switch (item.getItemId()) {
+        case R.id.overview:
+            mSampleView.showOverview();
+            return true;
+        case R.id.prev:
+            mSampleView.previousSample();
+            return true;
+        case R.id.next:
+            mSampleView.nextSample();
+            return true;
+        case R.id.toggle_rendering:
+            mSampleView.toggleRenderingMode();
+            return true;
+        case R.id.slideshow:
+            mSampleView.toggleSlideshow();
+            return true;
+        case R.id.fps:
+            mSampleView.toggleFPS();
+            return true;
+        case R.id.tiling:
+            mSampleView.toggleTiling();
+            return true;
+        case R.id.bbox:
+            mSampleView.toggleBBox();
+            return true;
+        case R.id.save_to_pdf:
+            mSampleView.saveToPDF();
+            return true;
+        case R.id.glcontext_opengles:
+            return setOpenGLContextSettings(false, 0);
+        case R.id.glcontext_msaa4_opengles:
+            return setOpenGLContextSettings(false, 4);
+        case R.id.glcontext_opengl:
+            return setOpenGLContextSettings(true, 0);
+        case R.id.glcontext_msaa4_opengl:
+            return setOpenGLContextSettings(true, 4);
+        default:
+            return false;
+        }
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        final int keycode = event.getKeyCode();
+        if (keycode == KeyEvent.KEYCODE_BACK) {
+            if (event.getAction() == KeyEvent.ACTION_UP) {
+                finish();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private static final int SET_TITLE = 1;
+    private static final int SET_SLIDES = 2;
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+            case SET_TITLE:
+                mTitle.setText((String) msg.obj);
+                SkiaSampleActivity.this.getActionBar().setSubtitle((String) msg.obj);
+                break;
+            case SET_SLIDES:
+                mSlideList.addAll((String[]) msg.obj);
+                break;
+            default:
+                break;
+            }
+        }
+    };
+
+    // Called by JNI
+    @Override
+    public void setTitle(CharSequence title) {
+        mHandler.obtainMessage(SET_TITLE, title).sendToTarget();
+    }
+
+    // Called by JNI
+    public void setSlideList(String[] slideList) {
+        mHandler.obtainMessage(SET_SLIDES, slideList).sendToTarget();
+    }
+
+    private boolean setOpenGLContextSettings(boolean requestedOpenGLAPI, int requestedSampleCount) {
+        if (mSampleView != null &&
+                mSampleView.getMSAASampleCount() == requestedSampleCount &&
+                mSampleView.getUsesOpenGLAPI() == requestedOpenGLAPI) {
+            return true;
+        }
+
+        createSampleView(requestedOpenGLAPI, requestedSampleCount);
+
+        return true;
+    }
+}
diff --git a/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleRenderer.java b/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleRenderer.java
new file mode 100644
index 0000000..5525709
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleRenderer.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package com.skia;
+
+import android.opengl.GLSurfaceView;
+import android.os.Handler;
+import android.util.Log;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+public class SkiaSampleRenderer implements GLSurfaceView.Renderer {
+
+    private final SkiaSampleView mSampleView;
+    private Handler mHandler = new Handler();
+    private int mMSAASampleCount;
+    private String mCmdLineFlags;
+
+    SkiaSampleRenderer(SkiaSampleView view, String cmdLineFlags) {
+        mSampleView = view;
+        mCmdLineFlags = cmdLineFlags;
+    }
+
+    @Override
+    public void onDrawFrame(GL10 gl) {
+        draw();
+    }
+
+    @Override
+    public void onSurfaceChanged(GL10 gl, int width, int height) {
+        updateSize(width, height);
+    }
+
+    @Override
+    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+        if (gl instanceof GL11) {
+            int value[] = new int[1];
+            ((GL11) gl).glGetIntegerv(GL11.GL_SAMPLES, value, 0);
+            if (value[0] == 1) {
+                mMSAASampleCount = 0;
+            } else {
+                mMSAASampleCount = value[0];
+            }
+        }
+
+        gl.glClearStencil(0);
+        gl.glClear(GL10.GL_STENCIL_BUFFER_BIT);
+
+        init((SkiaSampleActivity)mSampleView.getContext(), mCmdLineFlags, mMSAASampleCount);
+    }
+
+    // Called by JNI and the view.
+    synchronized public int getMSAASampleCount() {
+        return mMSAASampleCount;
+    }
+
+    // Called by JNI
+    private void startTimer(int ms) {
+        // After the delay, queue an event to the Renderer's thread
+        // to handle the event on the timer queue
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                mSampleView.queueEvent(new Runnable() {
+                    @Override
+                    public void run() {
+                        serviceQueueTimer();
+                    }
+                });
+            }
+        }, ms);
+    }
+
+    // Called by JNI
+    private void queueSkEvent() {
+        mSampleView.queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                processSkEvent();
+            }
+        });
+    }
+
+    // Called by JNI
+    private void requestRender() {
+        mSampleView.requestRender();
+    }
+
+    native void init(SkiaSampleActivity activity, String flags, int msaaSampleCount);
+    native void term();
+    native void draw();
+    native void updateSize(int w, int h);
+    native void handleClick(int owner, float x, float y, int state);
+    native void showOverview();
+    native void nextSample();
+    native void previousSample();
+    native void goToSample(int position);
+    native void toggleRenderingMode();
+    native void toggleSlideshow();
+    native void toggleFPS();
+    native void toggleTiling();
+    native void toggleBBox();
+    native void processSkEvent();
+    native void serviceQueueTimer();
+    native void saveToPDF();
+    native void postInval();
+}
\ No newline at end of file
diff --git a/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleView.java b/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleView.java
new file mode 100644
index 0000000..6213a6f
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/java/com/skia/SkiaSampleView.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package com.skia;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.content.Context;
+import android.opengl.EGL14;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.widget.Toast;
+
+public class SkiaSampleView extends GLSurfaceView {
+
+    private final SkiaSampleRenderer mSampleRenderer;
+    private boolean mRequestedOpenGLAPI; // true == use (desktop) OpenGL. false == use OpenGL ES.
+    private int mRequestedMSAASampleCount;
+
+    public SkiaSampleView(Context ctx, String cmdLineFlags, boolean useOpenGL, int msaaSampleCount) {
+        super(ctx);
+
+        mSampleRenderer = new SkiaSampleRenderer(this, cmdLineFlags);
+        mRequestedMSAASampleCount = msaaSampleCount;
+
+        setEGLContextClientVersion(2);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            setEGLConfigChooser(8, 8, 8, 8, 0, 8);
+        } else {
+            mRequestedOpenGLAPI = useOpenGL;
+            setEGLConfigChooser(new SampleViewEGLConfigChooser());
+        }
+        setRenderer(mSampleRenderer);
+        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int count = event.getPointerCount();
+        for (int i = 0; i < count; i++) {
+            final float x = event.getX(i);
+            final float y = event.getY(i);
+            final int owner = event.getPointerId(i);
+            int action = event.getAction() & MotionEvent.ACTION_MASK;
+            switch (action) {
+            case MotionEvent.ACTION_POINTER_UP:
+                action = MotionEvent.ACTION_UP;
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                action = MotionEvent.ACTION_DOWN;
+                break;
+            default:
+                break;
+            }
+            final int finalAction = action;
+            queueEvent(new Runnable() {
+                @Override
+                public void run() {
+                    mSampleRenderer.handleClick(owner, x, y, finalAction);
+                }
+            });
+        }
+        return true;
+    }
+
+    public void inval() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.postInval();
+            }
+        });
+    }
+
+    public void terminate() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.term();
+            }
+        });
+    }
+
+    public void showOverview() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.showOverview();
+            }
+        });
+    }
+
+    public void nextSample() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.nextSample();
+            }
+        });
+    }
+
+    public void previousSample() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.previousSample();
+            }
+        });
+    }
+
+    public void goToSample(final int position) {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.goToSample(position);
+            }
+        });
+    }
+
+    public void toggleRenderingMode() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.toggleRenderingMode();
+            }
+        });
+    }
+
+    public void toggleSlideshow() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.toggleSlideshow();
+            }
+        });
+    }
+
+    public void toggleFPS() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.toggleFPS();
+            }
+        });
+    }
+
+    public void toggleTiling() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.toggleTiling();
+            }
+        });
+    }
+
+    public void toggleBBox() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.toggleBBox();
+            }
+        });
+    }
+    
+    public void saveToPDF() {
+        queueEvent(new Runnable() {
+            @Override
+            public void run() {
+                mSampleRenderer.saveToPDF();
+            }
+        });
+
+	String msg = getContext().getString(R.string.pdf_save_info);
+	Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
+    }
+
+    public boolean getUsesOpenGLAPI() {
+        return mRequestedOpenGLAPI;
+    }
+
+    public int getMSAASampleCount() {
+        return mSampleRenderer.getMSAASampleCount();
+    }
+
+    private class SampleViewEGLConfigChooser implements GLSurfaceView.EGLConfigChooser {
+
+        @Override
+        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+            int numConfigs = 0;
+            int[] configSpec = null;
+            int[] value = new int[1];
+            
+            int[] validAPIs = new int[] {
+                EGL14.EGL_OPENGL_API,
+                EGL14.EGL_OPENGL_ES_API
+            };
+            int initialAPI = mRequestedOpenGLAPI ? 0 : 1;
+            
+            for (int i = initialAPI; i < validAPIs.length && numConfigs == 0; i++) {
+                int currentAPI = validAPIs[i];
+                EGL14.eglBindAPI(currentAPI);
+
+                // setup the renderableType which will only be included in the
+                // spec if we are attempting to get access to the OpenGL APIs.
+                int renderableType = EGL14.EGL_OPENGL_BIT;
+                if (currentAPI == EGL14.EGL_OPENGL_API) {
+                    renderableType = EGL14.EGL_OPENGL_ES2_BIT;
+                }
+
+                if (mRequestedMSAASampleCount > 0) {
+                    configSpec = new int[] {
+                        EGL10.EGL_RED_SIZE, 8,
+                        EGL10.EGL_GREEN_SIZE, 8,
+                        EGL10.EGL_BLUE_SIZE, 8,
+                        EGL10.EGL_ALPHA_SIZE, 8,
+                        EGL10.EGL_DEPTH_SIZE, 0,
+                        EGL10.EGL_STENCIL_SIZE, 8,
+                        EGL10.EGL_SAMPLE_BUFFERS, 1,
+                        EGL10.EGL_SAMPLES, mRequestedMSAASampleCount,
+                        EGL10.EGL_RENDERABLE_TYPE, renderableType,
+                        EGL10.EGL_NONE
+                    };
+
+                    // EGL_RENDERABLE_TYPE is only needed when attempting to use
+                    // the OpenGL API (not ES) and causes many EGL drivers to fail
+                    // with a BAD_ATTRIBUTE error.
+                    if (!mRequestedOpenGLAPI) {
+                      configSpec[16] = EGL10.EGL_NONE;
+                      Log.i("Skia", "spec: " + configSpec);
+                    }
+                    
+                    if (!egl.eglChooseConfig(display, configSpec, null, 0, value)) {
+                        Log.i("Skia", "Could not get MSAA context count: " + mRequestedMSAASampleCount);
+                    }
+
+                    numConfigs = value[0];
+                }
+
+                if (numConfigs <= 0) {
+                    // Try without multisampling.
+                    configSpec = new int[] {
+                        EGL10.EGL_RED_SIZE, 8,
+                        EGL10.EGL_GREEN_SIZE, 8,
+                        EGL10.EGL_BLUE_SIZE, 8,
+                        EGL10.EGL_ALPHA_SIZE, 8,
+                        EGL10.EGL_DEPTH_SIZE, 0,
+                        EGL10.EGL_STENCIL_SIZE, 8,
+                        EGL10.EGL_RENDERABLE_TYPE, renderableType,
+                        EGL10.EGL_NONE
+                    };
+                    
+                    // EGL_RENDERABLE_TYPE is only needed when attempting to use
+                    // the OpenGL API (not ES) and causes many EGL drivers to fail
+                    // with a BAD_ATTRIBUTE error.
+                    if (!mRequestedOpenGLAPI) {
+                      configSpec[12] = EGL10.EGL_NONE;
+                      Log.i("Skia", "spec: " + configSpec);
+                    }
+
+                    if (!egl.eglChooseConfig(display, configSpec, null, 0, value)) {
+                      Log.i("Skia", "Could not get non-MSAA context count");
+                    }
+                    numConfigs = value[0];
+                }
+            }
+
+            if (numConfigs <= 0) {
+                throw new IllegalArgumentException("No configs match configSpec");
+            }
+
+            // Get all matching configurations.
+            EGLConfig[] configs = new EGLConfig[numConfigs];
+            if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, value)) {
+                throw new IllegalArgumentException("Could not get config data");
+            }
+
+            for (int i = 0; i < configs.length; ++i) {
+                EGLConfig config = configs[i];
+                if (findConfigAttrib(egl, display, config , EGL10.EGL_RED_SIZE, 0) == 8 &&
+                        findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0) == 8 &&
+                        findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0) == 8 &&
+                        findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0) == 8 &&
+                        findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0) == 8) {
+                    return config;
+                }
+            }
+
+            throw new IllegalArgumentException("Could not find suitable EGL config");
+        }
+
+        private int findConfigAttrib(EGL10 egl, EGLDisplay display,
+                EGLConfig config, int attribute, int defaultValue) {
+            int[] value = new int[1];
+            if (egl.eglGetConfigAttrib(display, config, attribute, value)) {
+                return value[0];
+            }
+            return defaultValue;
+        }
+
+    }
+}
diff --git a/platform_tools/android/apps/sample_app/src/main/jni/AndroidKeyToSkKey.h b/platform_tools/android/apps/sample_app/src/main/jni/AndroidKeyToSkKey.h
new file mode 100644
index 0000000..00fd5d0
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/jni/AndroidKeyToSkKey.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2011 Skia
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+
+#ifndef _ANDROID_TO_SKIA_KEYCODES_H
+#define _ANDROID_TO_SKIA_KEYCODES_H
+
+#include "android/keycodes.h"
+#include "SkKey.h"
+
+// Convert an Android keycode to an SkKey.  This is an incomplete list, only
+// including keys used by the sample app.
+SkKey AndroidKeycodeToSkKey(int keycode) {
+    switch (keycode) {
+        case AKEYCODE_DPAD_LEFT:
+            return kLeft_SkKey;
+        case AKEYCODE_DPAD_RIGHT:
+            return kRight_SkKey;
+        case AKEYCODE_DPAD_UP:
+            return kUp_SkKey;
+        case AKEYCODE_DPAD_DOWN:
+            return kDown_SkKey;
+        case AKEYCODE_BACK:
+            return kBack_SkKey;
+        default:
+            return kNONE_SkKey;
+    }
+}
+
+#endif
diff --git a/platform_tools/android/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.cpp b/platform_tools/android/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.cpp
new file mode 100644
index 0000000..f233a2f
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.cpp
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2011 Skia
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#include "com_skia_SkiaSampleRenderer.h"
+
+#include "SampleApp.h"
+#include "SkApplication.h"
+#include "SkCanvas.h"
+#include "SkDevice.h"
+#include "SkEvent.h"
+#include "SkWindow.h"
+
+#include <jni.h>
+#include "AndroidKeyToSkKey.h"
+
+
+///////////////////////////////////////////
+///////////////// Globals /////////////////
+///////////////////////////////////////////
+
+struct ActivityGlue {
+    JNIEnv* m_env;
+    jweak m_obj;
+    jmethodID m_setTitle;
+    jmethodID m_setSlideList;
+    ActivityGlue() {
+        m_env = nullptr;
+        m_obj = nullptr;
+        m_setTitle = nullptr;
+        m_setSlideList = nullptr;
+    }
+} gActivityGlue;
+
+struct WindowGlue {
+    jweak m_obj;
+    jmethodID m_inval;
+    jmethodID m_queueSkEvent;
+    jmethodID m_startTimer;
+    jmethodID m_getMSAASampleCount;
+    WindowGlue() {
+        m_obj = nullptr;
+        m_inval = nullptr;
+        m_queueSkEvent = nullptr;
+        m_startTimer = nullptr;
+        m_getMSAASampleCount = nullptr;
+    }
+} gWindowGlue;
+
+SampleWindow* gWindow;
+
+///////////////////////////////////////////
+///////////// SkOSWindow impl /////////////
+///////////////////////////////////////////
+
+SkOSWindow::SkOSWindow(void*) : fDestroyRequested(false) {
+}
+
+SkOSWindow::~SkOSWindow() {
+}
+
+bool SkOSWindow::attach(SkBackEndTypes /* attachType */, int /*msaaSampleCount*/,
+                        bool /*deepColor*/, AttachmentInfo* info)
+{
+    JNIEnv* env = gActivityGlue.m_env;
+    if (!env || !gWindowGlue.m_getMSAASampleCount || !gWindowGlue.m_obj) {
+        return false;
+    }
+    if (env->IsSameObject(gWindowGlue.m_obj, nullptr)) {
+        SkDebugf("ERROR: The JNI WeakRef to the Window is invalid");
+        return false;
+    }
+    info->fSampleCount = env->CallIntMethod(gWindowGlue.m_obj, gWindowGlue.m_getMSAASampleCount);
+
+    // This is the value requested in SkiaSampleView.java.
+    info->fStencilBits = 8;
+    return true;
+}
+
+void SkOSWindow::release() {
+}
+
+void SkOSWindow::present() {
+}
+
+void SkOSWindow::closeWindow() {
+}
+
+void SkOSWindow::setVsync(bool) {
+}
+
+void SkOSWindow::onSetTitle(const char title[])
+{
+    JNIEnv* env = gActivityGlue.m_env;
+    if (!env) {
+        return;
+    }
+    if (env->IsSameObject(gActivityGlue.m_obj, nullptr)) {
+        SkDebugf("ERROR: The JNI WeakRef to the Activity is invalid");
+        return;
+    }
+
+    jstring string = env->NewStringUTF(title);
+    env->CallVoidMethod(gActivityGlue.m_obj, gActivityGlue.m_setTitle, string);
+    env->DeleteLocalRef(string);
+}
+
+void SkOSWindow::onHandleInval(const SkIRect& rect)
+{
+    JNIEnv* env = gActivityGlue.m_env;
+    if (!env || !gWindowGlue.m_inval || !gWindowGlue.m_obj) {
+        return;
+    }
+    if (env->IsSameObject(gWindowGlue.m_obj, nullptr)) {
+        SkDebugf("ERROR: The JNI WeakRef to the Window is invalid");
+        return;
+    }
+    env->CallVoidMethod(gWindowGlue.m_obj, gWindowGlue.m_inval);
+}
+
+///////////////////////////////////////////
+/////////////// SkEvent impl //////////////
+///////////////////////////////////////////
+
+void SkEvent::SignalQueueTimer(SkMSec ms)
+{
+    JNIEnv* env = gActivityGlue.m_env;
+    if (!env || !gWindowGlue.m_startTimer || !gWindowGlue.m_obj || !ms) {
+        return;
+    }
+    if (env->IsSameObject(gWindowGlue.m_obj, nullptr)) {
+        SkDebugf("ERROR: The JNI WeakRef to the Window is invalid");
+        return;
+    }
+    env->CallVoidMethod(gWindowGlue.m_obj,
+            gWindowGlue.m_startTimer, ms);
+}
+
+void SkEvent::SignalNonEmptyQueue()
+{
+    JNIEnv* env = gActivityGlue.m_env;
+    if (!env || !gWindowGlue.m_queueSkEvent || !gWindowGlue.m_obj) {
+        return;
+    }
+    if (env->IsSameObject(gWindowGlue.m_obj, nullptr)) {
+        SkDebugf("ERROR: The JNI WeakRef to the Window is invalid");
+        return;
+    }
+    env->CallVoidMethod(gWindowGlue.m_obj, gWindowGlue.m_queueSkEvent);
+}
+
+///////////////////////////////////////////
+////////////////// JNI ////////////////////
+///////////////////////////////////////////
+
+static jmethodID GetJMethod(JNIEnv* env, jclass clazz, const char name[],
+        const char signature[])
+{
+    jmethodID m = env->GetMethodID(clazz, name, signature);
+    if (!m) SkDebugf("Could not find Java method %s\n", name);
+    return m;
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_init(JNIEnv* env,
+        jobject thiz, jobject jsampleActivity, jstring cmdLineFlags, jint msaaSampleCount)
+{
+    // setup jni hooks to the java activity
+    gActivityGlue.m_env = env;
+    jclass clazz = env->FindClass("com/skia/SkiaSampleActivity");
+    gActivityGlue.m_obj = env->NewWeakGlobalRef(jsampleActivity);
+    gActivityGlue.m_setTitle = GetJMethod(env, clazz, "setTitle", "(Ljava/lang/CharSequence;)V");
+    gActivityGlue.m_setSlideList = GetJMethod(env, clazz, "setSlideList", "([Ljava/lang/String;)V");
+    env->DeleteLocalRef(clazz);
+
+    // setup jni hooks to the java renderer
+    clazz = env->FindClass("com/skia/SkiaSampleRenderer");
+    gWindowGlue.m_obj = env->NewWeakGlobalRef(thiz);
+    gWindowGlue.m_inval = GetJMethod(env, clazz, "requestRender", "()V");
+    gWindowGlue.m_queueSkEvent = GetJMethod(env, clazz, "queueSkEvent", "()V");
+    gWindowGlue.m_startTimer = GetJMethod(env, clazz, "startTimer", "(I)V");
+    gWindowGlue.m_getMSAASampleCount = GetJMethod(env, clazz, "getMSAASampleCount", "()I");
+    env->DeleteLocalRef(clazz);
+
+    application_init();
+
+    const char* flags = env->GetStringUTFChars(cmdLineFlags, JNI_FALSE);
+    SkTArray<SkString> flagEntries;
+    SkStrSplit(flags, " ", &flagEntries);
+
+    SkTArray<const char*> args;
+    args.push_back("SampleApp");
+    for (int i = 0; i < flagEntries.count(); i++) {
+        SkDebugf(flagEntries[i].c_str());
+        args.push_back(flagEntries[i].c_str());
+    }
+
+    SkString msaaSampleCountString;
+    if (msaaSampleCount > 0) {
+        args.push_back("--msaa");
+        msaaSampleCountString.appendS32(static_cast<uint32_t>(msaaSampleCount));
+        args.push_back(msaaSampleCountString.c_str());
+    }
+
+    if (gWindow) {
+        SkDebugf("The sample window already exists.");
+    } else {
+        gWindow = new SampleWindow(nullptr, args.count(), const_cast<char**>(args.begin()), nullptr);
+    }
+
+    // cleanup the command line flags
+    env->ReleaseStringUTFChars(cmdLineFlags, flags);
+
+    // send the list of slides up to the activity
+    const int slideCount = gWindow->sampleCount();
+    jobjectArray slideList = env->NewObjectArray(slideCount, env->FindClass("java/lang/String"), env->NewStringUTF(""));
+    for (int i = 0; i < slideCount; i++) {
+        jstring slideTitle = env->NewStringUTF(gWindow->getSampleTitle(i).c_str());
+        env->SetObjectArrayElement(slideList, i, slideTitle);
+        env->DeleteLocalRef(slideTitle);
+    }
+    env->CallVoidMethod(gActivityGlue.m_obj, gActivityGlue.m_setSlideList, slideList);
+    env->DeleteLocalRef(slideList);
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_term(JNIEnv* env,
+        jobject thiz)
+{
+    delete gWindow;
+    gWindow = nullptr;
+    application_term();
+    if (gWindowGlue.m_obj) {
+        env->DeleteWeakGlobalRef(gWindowGlue.m_obj);
+        gWindowGlue.m_obj = nullptr;
+    }
+    if (gActivityGlue.m_obj) {
+        env->DeleteWeakGlobalRef(gActivityGlue.m_obj);
+        gActivityGlue.m_obj = nullptr;
+    }
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_draw(
+        JNIEnv* env, jobject thiz)
+{
+    if (!gWindow) return;
+    gWindow->update(nullptr);
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_updateSize(JNIEnv* env,
+        jobject thiz, jint w, jint h)
+{
+    gWindow->resize(w, h);
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_handleClick(JNIEnv* env,
+        jobject thiz, jint owner, jfloat x, jfloat y, jint jstate)
+{
+    SkView::Click::State state;
+    switch(jstate) {
+        case 0:     // MotionEvent.ACTION_DOWN
+            state = SkView::Click::kDown_State;
+            break;
+        case 1:     // MotionEvent.ACTION_UP
+        case 3:     // MotionEvent.ACTION_CANCEL
+            state = SkView::Click::kUp_State;
+            break;
+        case 2:     // MotionEvent.ACTION_MOVE
+            state = SkView::Click::kMoved_State;
+            break;
+        default:
+            SkDebugf("motion event ignored\n");
+            return;
+    }
+    gWindow->handleClick(x, y, state, reinterpret_cast<void*>(owner));
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_nextSample(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->nextSample();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_previousSample(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->previousSample();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_goToSample(
+        JNIEnv* env, jobject thiz, jint position)
+{
+    gWindow->goToSample(position);
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleRenderingMode(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->toggleRendering();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_showOverview(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->showOverview();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleSlideshow(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->toggleSlideshow();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleFPS(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->toggleFPS();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleTiling(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->handleChar('t');
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleBBox(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->handleChar('b');
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_processSkEvent(
+        JNIEnv* env, jobject thiz)
+{
+    if (SkEvent::ProcessEvent()) {
+        SkEvent::SignalNonEmptyQueue();
+    }
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_serviceQueueTimer(
+        JNIEnv* env, jobject thiz)
+{
+    SkEvent::ServiceQueueTimer();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_saveToPDF(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->saveToPdf();
+}
+
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_postInval(
+        JNIEnv* env, jobject thiz)
+{
+    gWindow->postInvalDelay();
+}
diff --git a/platform_tools/android/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.h b/platform_tools/android/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.h
new file mode 100644
index 0000000..8883a3a
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.h
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2015 Skia
+ *
+ * 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 com_skia_SkiaSampleRenderer */
+
+#ifndef _Included_com_skia_SkiaSampleRenderer
+#define _Included_com_skia_SkiaSampleRenderer
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    init
+ * Signature: (Lcom/skia/SkiaSampleActivity;Ljava/lang/String;I)V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_init
+  (JNIEnv *, jobject, jobject, jstring, jint);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    term
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_term
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    draw
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_draw
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    updateSize
+ * Signature: (II)V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_updateSize
+  (JNIEnv *, jobject, jint, jint);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    handleClick
+ * Signature: (IFFI)V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_handleClick
+  (JNIEnv *, jobject, jint, jfloat, jfloat, jint);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    showOverview
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_showOverview
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    nextSample
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_nextSample
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    previousSample
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_previousSample
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    goToSample
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_goToSample
+  (JNIEnv *, jobject, jint);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    toggleRenderingMode
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleRenderingMode
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    toggleSlideshow
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleSlideshow
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    toggleFPS
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleFPS
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    toggleTiling
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleTiling
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    toggleBBox
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_toggleBBox
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    processSkEvent
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_processSkEvent
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    serviceQueueTimer
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_serviceQueueTimer
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    saveToPdf
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_saveToPDF
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_skia_SkiaSampleRenderer
+ * Method:    postInval
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_skia_SkiaSampleRenderer_postInval
+  (JNIEnv *, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/platform_tools/android/apps/sample_app/src/main/res/drawable-hdpi/ic_btn_find_next.png b/platform_tools/android/apps/sample_app/src/main/res/drawable-hdpi/ic_btn_find_next.png
new file mode 100644
index 0000000..b696a6b
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/res/drawable-hdpi/ic_btn_find_next.png
Binary files differ
diff --git a/platform_tools/android/apps/sample_app/src/main/res/drawable-hdpi/ic_btn_find_prev.png b/platform_tools/android/apps/sample_app/src/main/res/drawable-hdpi/ic_btn_find_prev.png
new file mode 100644
index 0000000..5550c5a
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/res/drawable-hdpi/ic_btn_find_prev.png
Binary files differ
diff --git a/platform_tools/android/apps/sample_app/src/main/res/layout/layout.xml b/platform_tools/android/apps/sample_app/src/main/res/layout/layout.xml
new file mode 100644
index 0000000..cdb90e9
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/res/layout/layout.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2011 Google Inc.
+
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/holder"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <TextView android:id="@+id/title_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        />
+</LinearLayout>
+
diff --git a/platform_tools/android/apps/sample_app/src/main/res/menu/action_bar.xml b/platform_tools/android/apps/sample_app/src/main/res/menu/action_bar.xml
new file mode 100644
index 0000000..72e135e
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/res/menu/action_bar.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2011 Google Inc.
+
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/fps"
+        android:title="@string/fps"
+        android:showAsAction="ifRoom"
+        />
+    <item
+        android:id="@+id/toggle_rendering"
+        android:title="@string/toggle_rendering"
+        android:showAsAction="ifRoom"
+        />
+    <item
+        android:id="@+id/prev"
+        android:showAsAction="always"
+        android:icon="@drawable/ic_btn_find_prev"
+        />
+    <item
+        android:id="@+id/next"
+        android:showAsAction="always"
+        android:icon="@drawable/ic_btn_find_next"
+        />
+    <item
+        android:id="@+id/overview"
+        android:title="@string/overview"
+        />
+    <item
+        android:id="@+id/slideshow"
+        android:title="@string/slideshow"
+        />
+    <item
+        android:id="@+id/tiling"
+        android:title="@string/tiling"
+        />
+    <item
+        android:id="@+id/bbox"
+        android:title="@string/bbox"
+        />
+    <item
+        android:id="@+id/glcontext_menu"
+        android:title="@string/glcontext_menu">
+        <menu>
+            <item
+                android:id="@+id/glcontext_opengles"
+                android:title="@string/glcontext_opengles"
+                android:checkable="true"
+                />
+            <item
+                android:id="@+id/glcontext_msaa4_opengles"
+                android:title="@string/glcontext_msaa4_opengles"
+                android:checkable="true"
+                />
+            <item
+                android:id="@+id/glcontext_opengl"
+                android:title="@string/glcontext_opengl"
+                android:checkable="true"
+                />
+            <item
+                android:id="@+id/glcontext_msaa4_opengl"
+                android:title="@string/glcontext_msaa4_opengl"
+                android:checkable="true"
+                />
+        </menu>
+    </item>
+    <item
+        android:id="@+id/save_to_pdf"
+        android:title="@string/save_to_pdf"
+        />
+</menu>
diff --git a/platform_tools/android/apps/sample_app/src/main/res/values/strings.xml b/platform_tools/android/apps/sample_app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..f9ec6a0
--- /dev/null
+++ b/platform_tools/android/apps/sample_app/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">Skia Samples</string>
+    <string name="overview">Overview</string>
+    <string name="toggle_rendering">Toggle Rendering</string>
+    <string name="slideshow">Slideshow</string>
+    <string name="fps">FPS</string>
+    <string name="tiling">Toggle Tiling</string>
+    <string name="bbox">Toggle SKP BBox</string>
+    <string name="save_to_pdf">Save to PDF</string>
+    <string name="pdf_save_info">View saved as PDF to directory \"/sdcard\"</string>
+    <string name="glcontext_menu">Set OpenGL Context Type</string>
+    <string name="glcontext_opengles">OpenGL ES</string>
+    <string name="glcontext_msaa4_opengles">OpenGL ES, MSAA4</string>
+    <string name="glcontext_opengl">OpenGL</string>
+    <string name="glcontext_msaa4_opengl">OpenGL, MSAA4</string>
+</resources>
diff --git a/platform_tools/android/apps/settings.gradle b/platform_tools/android/apps/settings.gradle
index faa3934..9d23bff 100644
--- a/platform_tools/android/apps/settings.gradle
+++ b/platform_tools/android/apps/settings.gradle
@@ -1,2 +1,3 @@
+include ':sample_app'
 include ':canvasproof'
 include ':viewer'
diff --git a/platform_tools/android/gyp/dependencies.gypi b/platform_tools/android/gyp/dependencies.gypi
index 55f9576..14f73bf 100644
--- a/platform_tools/android/gyp/dependencies.gypi
+++ b/platform_tools/android/gyp/dependencies.gypi
@@ -89,5 +89,37 @@
         ],
       },
     },
+    {
+      # This target is a dependency for Skia Sample application which runs on
+      # Android.  Since Android requires us to load native code in shared
+      # libraries, we need a common entry point to wrap around main(). Here
+      # we also change the type of all would-be executables to be shared
+      # libraries.  The alternative would be to introduce a condition in every
+      # executable target which changes to a shared library if the target OS is
+      # Android.  This is nicer because the switch is in one place.
+      'target_name': 'Android_SampleApp',
+      'type': 'static_library',
+      'direct_dependent_settings': {
+        'target_conditions': [
+          # '_type' is an 'automatic variable' which is defined for any
+          # target which defines a key-value pair with 'type' as the key (so,
+          # all of them).  Conditionals inside 'target_conditions' are evaluated
+          # *after* all other definitions and conditionals are evaluated, so
+          # we're guaranteed that '_type' will be defined when we get here.
+          # For more info, see:
+          # - http://code.google.com/p/gyp/wiki/InputFormatReference#Variables
+          # - http://codereview.appspot.com/6353065/
+          ['_type == "executable"', {
+            'type': 'shared_library',
+          }],
+        ],
+        'cflags': [
+          '-Wno-unused-private-field',
+        ],
+        'sources': [
+          '../apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.cpp',
+        ],
+      },
+    },
   ]
 }
diff --git a/platform_tools/android/gyp/skia_android.gypi b/platform_tools/android/gyp/skia_android.gypi
index 87c4714..fe535b3 100644
--- a/platform_tools/android/gyp/skia_android.gypi
+++ b/platform_tools/android/gyp/skia_android.gypi
@@ -51,4 +51,72 @@
       'canvasproof.gypi',
       'viewer.gypi',
   ],
+  'targets': [
+    {
+      'target_name': 'CopySampleAppDeps',
+      'type': 'none',
+      'dependencies': [
+        'skia_lib.gyp:skia_lib',
+        'SampleApp.gyp:SampleApp',
+      ],
+      'copies': [
+        # Copy all shared libraries into the Android app's libs folder.  Note
+        # that this copy requires us to build SkiaAndroidApp after those
+        # libraries, so that they exist by the time it occurs.  If there are no
+        # libraries to copy, this will cause an error in Make, but the app will
+        # still build.
+        {
+          'destination': '<(android_base)/apps/sample_app/src/main/libs/<(android_arch)',
+          'conditions': [
+            [ 'skia_shared_lib', {
+              'files': [
+                '<(SHARED_LIB_DIR)/libSampleApp.so',
+                '<(SHARED_LIB_DIR)/libskia_android.so',
+              ]}, {
+              'files': [
+                '<(SHARED_LIB_DIR)/libSampleApp.so',
+             ]}
+           ],
+          ],
+        },
+      ],
+    },
+    {
+      'target_name': 'SampleApp_APK',
+      'type': 'none',
+      'dependencies': [
+        'CopySampleAppDeps',
+      ],
+      'actions': [
+        {
+          'action_name': 'SampleApp_apk',
+          'inputs': [
+            '<(android_base)/apps/sample_app/src/main/AndroidManifest.xml',
+            '<(android_base)/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.h',
+            '<(android_base)/apps/sample_app/src/main/jni/com_skia_SkiaSampleRenderer.cpp',
+            '<(android_base)/apps/sample_app/src/main/java/com/skia/SkiaSampleActivity.java',
+            '<(android_base)/apps/sample_app/src/main/java/com/skia/SkiaSampleRenderer.java',
+            '<(android_base)/apps/sample_app/src/main/java/com/skia/SkiaSampleView.java',
+            '<(android_base)/apps/sample_app/src/main/libs/<(android_arch)/libSampleApp.so',
+          ],
+          'conditions': [
+            [ 'skia_shared_lib', {
+              'inputs': [
+                '<(android_base)/apps/sample_app/src/main/libs/<(android_arch)/libskia_android.so',
+              ],
+            }],
+          ],
+          'outputs': [
+            '<(android_base)/apps/sample_app/build/outputs/apk/sample_app-<(android_variant)-<(android_apk_suffix)',
+          ],
+          'action': [
+            '<(android_base)/apps/gradlew',
+            ':sample_app:assemble<(android_variant)<(android_buildtype)',
+            '-p<(android_base)/apps/sample_app',
+            '-PsuppressNativeBuild',
+          ],
+        },
+      ],
+    },
+  ],
 }