Now we can use drawer to view the state information of the native app, and set its state using the spinner.

BUG=skia:
GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2004633002

Committed: https://skia.googlesource.com/skia/+/4e4e30823fba0047b93a56bfcf05c04ca57e82ec

Review-Url: https://codereview.chromium.org/2004633002
diff --git a/platform_tools/android/apps/viewer/build.gradle b/platform_tools/android/apps/viewer/build.gradle
index 79cac8a..15cff2c 100644
--- a/platform_tools/android/apps/viewer/build.gradle
+++ b/platform_tools/android/apps/viewer/build.gradle
@@ -5,8 +5,14 @@
  * found in the LICENSE file.
  */
 apply plugin: 'com.android.application'
+
+dependencies {
+    compile 'com.android.support:support-v13:23.3.0'
+    compile 'com.android.support:appcompat-v7:23.3.0'
+}
+
 android {
-    compileSdkVersion 19
+    compileSdkVersion 23
     buildToolsVersion "22.0.1"
     defaultConfig {
         applicationId "org.skia.viewer"
diff --git a/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/StateAdapter.java b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/StateAdapter.java
new file mode 100644
index 0000000..4c9dcd4
--- /dev/null
+++ b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/StateAdapter.java
@@ -0,0 +1,114 @@
+package org.skia.viewer;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class StateAdapter extends BaseAdapter implements AdapterView.OnItemSelectedListener {
+    static final String NAME = "name";
+    static final String VALUE = "value";
+    static final String OPTIONS = "options";
+
+    ViewerActivity mViewerActivity;
+    JSONArray mStateJson;
+
+    public StateAdapter(ViewerActivity viewerActivity) {
+        mViewerActivity = viewerActivity;
+        try {
+            mStateJson = new JSONArray("[{\"name\": \"Please\", " +
+                    "\"value\": \"Initialize\", \"options\": []}]");
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void setState(String stateJson) {
+        try {
+            mStateJson = new JSONArray(stateJson);
+            notifyDataSetChanged();
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public int getCount() {
+        return mStateJson.length();
+    }
+
+    @Override
+    public Object getItem(int position) {
+        try {
+            return mStateJson.getJSONObject(position);
+        } catch (JSONException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return 0;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+            convertView = LayoutInflater.from(mViewerActivity).inflate(R.layout.state_item, null);
+        }
+        TextView nameText = (TextView) convertView.findViewById(R.id.nameText);
+        TextView valueText = (TextView) convertView.findViewById(R.id.valueText);
+        Spinner optionSpinner = (Spinner) convertView.findViewById(R.id.optionSpinner);
+        JSONObject stateObject = (JSONObject) getItem(position);
+        try {
+            nameText.setText(stateObject.getString(NAME));
+            String value = stateObject.getString(VALUE);
+            JSONArray options = stateObject.getJSONArray(OPTIONS);
+            if (options.length() == 0) {
+                valueText.setText(value);
+                valueText.setVisibility(View.VISIBLE);
+                optionSpinner.setVisibility(View.GONE);
+
+            } else {
+                ArrayList<String> optionList = new ArrayList<>();
+                String[] optionStrings = new String[options.length()];
+                for(int i=0; i<options.length(); i++) {
+                    optionList.add(options.getString(i));
+                }
+                optionSpinner.setAdapter(new ArrayAdapter<String>(mViewerActivity,
+                        android.R.layout.simple_spinner_dropdown_item, optionList));
+                optionSpinner.setSelection(optionList.indexOf(value));
+                optionSpinner.setOnItemSelectedListener(this);
+                optionSpinner.setVisibility(View.VISIBLE);
+                valueText.setVisibility(View.GONE);
+            }
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        return convertView;
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        View stateItem = (View) parent.getParent();
+        String stateName = ((TextView) stateItem.findViewById(R.id.nameText)).getText().toString();
+        String stateValue = ((TextView) view).getText().toString();
+        mViewerActivity.onStateChanged(stateName, stateValue);
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        // do nothing
+    }
+}
diff --git a/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerActivity.java b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerActivity.java
index 49f711d..ce5bb0d 100644
--- a/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerActivity.java
+++ b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerActivity.java
@@ -8,9 +8,10 @@
 package org.skia.viewer;
 
 import android.app.Activity;
+import android.content.res.Configuration;
 import android.os.Bundle;
-import android.util.Log;
-import android.view.GestureDetector;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -20,11 +21,17 @@
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
+import android.widget.ListView;
 
 public class ViewerActivity
         extends Activity implements SurfaceHolder.Callback, View.OnTouchListener {
     private static final float FLING_VELOCITY_THRESHOLD = 1000;
 
+    private DrawerLayout mDrawerLayout;
+    private ActionBarDrawerToggle mDrawerToggle;
+    private ListView mDrawerList;
+    private StateAdapter mStateAdapter;
+
     private SurfaceView mView;
     private ViewerApplication mApplication;
 
@@ -33,6 +40,7 @@
     private native void onSurfaceDestroyed(long handle);
     private native void onKeyPressed(long handle, int keycode);
     private native void onTouched(long handle, int owner, int state, float x, float y);
+    private native void onUIStateChanged(long handle, String stateName, String stateValue);
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
@@ -43,6 +51,12 @@
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
+        // Pass the event to ActionBarDrawerToggle, if it returns
+        // true, then it has handled the app icon touch event
+        if (mDrawerToggle.onOptionsItemSelected(item)) {
+            return true;
+        }
+
         switch (item.getItemId()) {
             case R.id.action_left:
                 onKeyPressed(mApplication.getNativeHandle(), KeyEvent.KEYCODE_SOFT_LEFT);
@@ -60,12 +74,36 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
 
-        mApplication = (ViewerApplication) getApplication();
-        mApplication.setViewerActivity(this);
         mView = (SurfaceView) findViewById(R.id.surfaceView);
         mView.getHolder().addCallback(this);
 
         mView.setOnTouchListener(this);
+
+        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout);
+        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
+                R.string.drawer_open, R.string.drawer_close);
+        mDrawerLayout.addDrawerListener(mDrawerToggle);
+        getActionBar().setDisplayHomeAsUpEnabled(true);
+        getActionBar().setHomeButtonEnabled(true);
+
+        mDrawerList = (ListView) findViewById(R.id.leftDrawer);
+        mStateAdapter = new StateAdapter(this);
+        mDrawerList.setAdapter(mStateAdapter);
+
+        mApplication = (ViewerApplication) getApplication();
+        mApplication.setViewerActivity(this);
+    }
+
+    @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+        mDrawerToggle.syncState();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDrawerToggle.onConfigurationChanged(newConfig);
     }
 
     @Override
@@ -107,4 +145,12 @@
         }
         return true;
     }
+
+    public void setState(String stateJson) {
+        mStateAdapter.setState(stateJson);
+    }
+
+    public void onStateChanged(String stateName, String stateValue) {
+        onUIStateChanged(mApplication.getNativeHandle(), stateName, stateValue);
+    }
 }
diff --git a/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerApplication.java b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerApplication.java
index 4b890bd..ee1695a 100644
--- a/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerApplication.java
+++ b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/ViewerApplication.java
@@ -12,6 +12,7 @@
 public class ViewerApplication extends Application {
     private long mNativeHandle = 0;
     private ViewerActivity mViewerActivity;
+    private String mStateJsonStr, mTitle;
 
     static {
         System.loadLibrary("skia_android");
@@ -41,16 +42,40 @@
     }
 
     public void setViewerActivity(ViewerActivity viewerActivity) {
-        this.mViewerActivity = viewerActivity;
+        mViewerActivity = viewerActivity;
+        // Note that viewerActivity might be null (called by onDestroy)
+        if (mViewerActivity != null) {
+            // A new ViewerActivity is created; initialize its state and title
+            if (mStateJsonStr != null) {
+                mViewerActivity.setState(mStateJsonStr);
+            }
+            if (mTitle != null) {
+                mViewerActivity.setTitle(mTitle);
+            }
+        }
     }
 
     public void setTitle(String title) {
-        final String finalTitle = title;
+        mTitle = title; // Similar to mStateJsonStr, we have to store this.
         if (mViewerActivity != null) {
             mViewerActivity.runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    mViewerActivity.setTitle(finalTitle);
+                    mViewerActivity.setTitle(mTitle);
+                }
+            });
+        }
+    }
+
+    public void setState(String stateJsonStr) {
+        // We have to store this state because ViewerActivity may be destroyed while the native app
+        // is still running. When a new ViewerActivity is created, we'll pass the state to it.
+        mStateJsonStr = stateJsonStr;
+        if (mViewerActivity != null) {
+            mViewerActivity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mViewerActivity.setState(mStateJsonStr);
                 }
             });
         }
diff --git a/platform_tools/android/apps/viewer/src/main/res/layout/activity_main.xml b/platform_tools/android/apps/viewer/src/main/res/layout/activity_main.xml
index 6597a48..985b67d 100644
--- a/platform_tools/android/apps/viewer/src/main/res/layout/activity_main.xml
+++ b/platform_tools/android/apps/viewer/src/main/res/layout/activity_main.xml
@@ -1,17 +1,36 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/mainLayout"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context=".ViewerActivity">
 
-    <SurfaceView
-        android:id="@+id/surfaceView"
+<android.support.v4.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/drawerLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <!-- The main content view -->
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:id="@+id/mainLayout"
+        android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_centerVertical="true"
-        android:layout_centerHorizontal="true" />
+        tools:context=".ViewerActivity">
 
-</LinearLayout>
+        <SurfaceView
+            android:id="@+id/surfaceView"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_centerVertical="true"
+            android:layout_centerHorizontal="true" />
+
+    </LinearLayout>
+
+    <!-- The navigation drawer -->
+    <ListView android:id="@+id/leftDrawer"
+        android:layout_width="240dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:choiceMode="singleChoice"
+        android:divider="@android:color/transparent"
+        android:dividerHeight="0dp"
+        android:background="@android:color/background_light"/>
+</android.support.v4.widget.DrawerLayout>
+
diff --git a/platform_tools/android/apps/viewer/src/main/res/layout/state_item.xml b/platform_tools/android/apps/viewer/src/main/res/layout/state_item.xml
new file mode 100644
index 0000000..7a7d539
--- /dev/null
+++ b/platform_tools/android/apps/viewer/src/main/res/layout/state_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:weightSum="1">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:layout_marginLeft="10dp"
+        android:layout_marginBottom="0dp"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:text="Name:"
+        android:id="@+id/nameText" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="10dp"
+        android:layout_marginLeft="10dp"
+        android:layout_marginTop="0dp"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="Value"
+        android:id="@+id/valueText" />
+
+    <Spinner
+        android:id="@+id/optionSpinner"
+        android:paddingTop="0dp"
+        android:paddingBottom="0dp"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+    </Spinner>
+
+</LinearLayout>
diff --git a/platform_tools/android/apps/viewer/src/main/res/values/strings.xml b/platform_tools/android/apps/viewer/src/main/res/values/strings.xml
new file mode 100644
index 0000000..582c566
--- /dev/null
+++ b/platform_tools/android/apps/viewer/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="drawer_open">Open navigation drawer</string>
+    <string name="drawer_close">Close navigation drawer</string>
+</resources>
\ No newline at end of file
diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp
index b450f21..9f3b8f2 100644
--- a/tools/viewer/Viewer.cpp
+++ b/tools/viewer/Viewer.cpp
@@ -35,7 +35,13 @@
     return viewer->onTouch(owner, state, x, y);
 }
 
-DEFINE_bool2(fullscreen, f, false, "Run fullscreen.");
+static void on_ui_state_changed_handler(const SkString& stateName, const SkString& stateValue, void* userData) {
+    Viewer* viewer = reinterpret_cast<Viewer*>(userData);
+
+    return viewer->onUIStateChanged(stateName, stateValue);
+}
+
+DEFINE_bool2(fullscreen, f, true, "Run fullscreen.");
 DEFINE_string(key, "", "Space-separated key/value pairs to add to JSON identifying this builder.");
 DEFINE_string2(match, m, nullptr,
                "[~][^]substring[$] [...] of bench name to run.\n"
@@ -54,6 +60,12 @@
     " [Vulkan]"
 };
 
+const char* kName = "name";
+const char* kValue = "value";
+const char* kOptions = "options";
+const char* kSlideStateName = "Slide";
+const char* kBackendStateName = "Backend";
+
 Viewer::Viewer(int argc, char** argv, void* platformData)
     : fCurrentMeasurement(0)
     , fDisplayStats(false)
@@ -83,6 +95,7 @@
     fCommands.attach(fWindow);
     fWindow->registerPaintFunc(on_paint_handler, this);
     fWindow->registerTouchFunc(on_touch_handler, this);
+    fWindow->registerUIStateChangedFunc(on_ui_state_changed_handler, this);
 
     // add key-bindings
     fCommands.addCommand('s', "Overlays", "Toggle stats display", [this]() {
@@ -220,6 +233,10 @@
 }
 
 void Viewer::setupCurrentSlide(int previousSlide) {
+    if (fCurrentSlide == previousSlide) {
+        return; // no change; do nothing
+    }
+
     fGesture.reset();
     fDefaultMatrix.reset();
     fDefaultMatrixInv.reset();
@@ -242,6 +259,7 @@
     }
 
     this->updateTitle();
+    this->updateUIState();
     fSlides[fCurrentSlide]->load();
     if (previousSlide >= 0) {
         fSlides[previousSlide]->unload();
@@ -389,3 +407,49 @@
         fWindow->inval();
     }
 }
+
+void Viewer::updateUIState() {
+    Json::Value slideState(Json::objectValue);
+    slideState[kName] = kSlideStateName;
+    slideState[kValue] = fSlides[fCurrentSlide]->getName().c_str();
+    Json::Value allSlideNames(Json::arrayValue);
+    for(auto slide : fSlides) {
+        allSlideNames.append(Json::Value(slide->getName().c_str()));
+    }
+    slideState[kOptions] = allSlideNames;
+
+    // This state is currently a demo for the one without options.
+    // We will be able to change the backend too.
+    Json::Value backendState(Json::objectValue);
+    backendState[kName] = kBackendStateName;
+    backendState[kValue] = fBackendType == sk_app::Window::kVulkan_BackendType ?
+            "Vulkan" : "Other than Vulkan";
+    backendState[kOptions] = Json::Value(Json::arrayValue);
+
+    Json::Value state(Json::arrayValue);
+    state.append(slideState);
+    state.append(backendState);
+
+    fWindow->setUIState(state);
+}
+
+void Viewer::onUIStateChanged(const SkString& stateName, const SkString& stateValue) {
+    // Currently, we only recognize the Slide state
+    if (stateName.equals(kSlideStateName)) {
+        int previousSlide = fCurrentSlide;
+        fCurrentSlide = 0;
+        for(auto slide : fSlides) {
+            if (slide->getName().equals(stateValue)) {
+                setupCurrentSlide(previousSlide);
+                break;
+            }
+            fCurrentSlide++;
+        }
+        if (fCurrentSlide >= fSlides.count()) {
+            fCurrentSlide = previousSlide;
+            SkDebugf("Slide not found: %s", stateValue.c_str());
+        }
+    } else {
+        SkDebugf("Unknown stateName: %s", stateName.c_str());
+    }
+}
diff --git a/tools/viewer/Viewer.h b/tools/viewer/Viewer.h
index 0bafee1..e752b3f 100644
--- a/tools/viewer/Viewer.h
+++ b/tools/viewer/Viewer.h
@@ -25,12 +25,15 @@
     void onPaint(SkCanvas* canvas);
     void onIdle(double ms) override;
     bool onTouch(int owner, sk_app::Window::InputState state, float x, float y);
+    void onUIStateChanged(const SkString& stateName, const SkString& stateValue);
 
 private:
     void initSlides();
     void updateTitle();
     void setupCurrentSlide(int previousSlide);
 
+    void updateUIState();
+
     void drawStats(SkCanvas* canvas);
 
     void changeZoomLevel(float delta);
diff --git a/tools/viewer/sk_app/Window.cpp b/tools/viewer/sk_app/Window.cpp
index ba905e0..1c798df 100644
--- a/tools/viewer/sk_app/Window.cpp
+++ b/tools/viewer/sk_app/Window.cpp
@@ -32,12 +32,16 @@
     return false;
 }
 
+static void default_ui_state_changed_func(
+        const SkString& stateName, const SkString& stateValue, void* userData) {}
+
 static void default_paint_func(SkCanvas*, void* userData) {}
 
 Window::Window() : fCharFunc(default_char_func)
                  , fKeyFunc(default_key_func)
                  , fMouseFunc(default_mouse_func)
                  , fTouchFunc(default_touch_func)
+                 , fUIStateChangedFunc(default_ui_state_changed_func)
                  , fPaintFunc(default_paint_func) {
 }
 
@@ -62,6 +66,10 @@
     return fTouchFunc(owner, state, x, y, fTouchUserData);
 }
 
+void Window::onUIStateChanged(const SkString& stateName, const SkString& stateValue) {
+    return fUIStateChangedFunc(stateName, stateValue, fUIStateChangedUserData);
+}
+
 void Window::onPaint() {
     markInvalProcessed();
     sk_sp<SkSurface> backbuffer = fWindowContext->getBackbufferSurface();
diff --git a/tools/viewer/sk_app/Window.h b/tools/viewer/sk_app/Window.h
index 65e1542..9ca4231 100644
--- a/tools/viewer/sk_app/Window.h
+++ b/tools/viewer/sk_app/Window.h
@@ -12,6 +12,7 @@
 #include "SkRect.h"
 #include "SkTouchGesture.h"
 #include "SkTypes.h"
+#include "SkJSONCPP.h"
 
 class SkCanvas;
 
@@ -27,6 +28,7 @@
 
     virtual void setTitle(const char*) = 0;
     virtual void show() = 0;
+    virtual void setUIState(const Json::Value& state) {}  // do nothing in default
 
     // Shedules an invalidation event for window if one is not currently pending.
     // Make sure that either onPaint or markInvalReceived is called when the client window consumes
@@ -110,6 +112,8 @@
     typedef bool(*OnKeyFunc)(Key key, InputState state, uint32_t modifiers, void* userData);
     typedef bool(*OnMouseFunc)(int x, int y, InputState state, uint32_t modifiers, void* userData);
     typedef bool(*OnTouchFunc)(int owner, InputState state, float x, float y, void* userData);
+    typedef void(*OnUIStateChangedFunc)(
+            const SkString& stateName, const SkString& stateValue, void* userData);
     typedef void(*OnPaintFunc)(SkCanvas*, void* userData);
 
     void registerCharFunc(OnCharFunc func, void* userData) {
@@ -137,10 +141,16 @@
         fTouchUserData = userData;
     }
 
+    void registerUIStateChangedFunc(OnUIStateChangedFunc func, void* userData) {
+        fUIStateChangedFunc = func;
+        fUIStateChangedUserData = userData;
+    }
+
     bool onChar(SkUnichar c, uint32_t modifiers);
     bool onKey(Key key, InputState state, uint32_t modifiers);
     bool onMouse(int x, int y, InputState state, uint32_t modifiers);
     bool onTouch(int owner, InputState state, float x, float y);  // multi-owner = multi-touch
+    void onUIStateChanged(const SkString& stateName, const SkString& stateValue);
     void onPaint();
     void onResize(uint32_t width, uint32_t height);
 
@@ -164,6 +174,9 @@
     void*        fMouseUserData;
     OnTouchFunc  fTouchFunc;
     void*        fTouchUserData;
+    OnUIStateChangedFunc
+                 fUIStateChangedFunc;
+    void*        fUIStateChangedUserData;
     OnPaintFunc  fPaintFunc;
     void*        fPaintUserData;
 
diff --git a/tools/viewer/sk_app/android/Window_android.cpp b/tools/viewer/sk_app/android/Window_android.cpp
index ed03c81..4f33870 100644
--- a/tools/viewer/sk_app/android/Window_android.cpp
+++ b/tools/viewer/sk_app/android/Window_android.cpp
@@ -41,6 +41,10 @@
     fSkiaAndroidApp->setTitle(title);
 }
 
+void Window_android::setUIState(const Json::Value& state) {
+    fSkiaAndroidApp->setUIState(state);
+}
+
 bool Window_android::attach(BackendType attachType, const DisplayParams& params) {
     if (kVulkan_BackendType != attachType) {
         return false;
diff --git a/tools/viewer/sk_app/android/Window_android.h b/tools/viewer/sk_app/android/Window_android.h
index f7d348b..f1c0798 100644
--- a/tools/viewer/sk_app/android/Window_android.h
+++ b/tools/viewer/sk_app/android/Window_android.h
@@ -28,6 +28,7 @@
 
     bool attach(BackendType attachType, const DisplayParams& params) override;
     void onInval() override;
+    void setUIState(const Json::Value& state) override;
 
     void paintIfNeeded();
 
diff --git a/tools/viewer/sk_app/android/surface_glue_android.cpp b/tools/viewer/sk_app/android/surface_glue_android.cpp
index 958b787..daf26a3 100644
--- a/tools/viewer/sk_app/android/surface_glue_android.cpp
+++ b/tools/viewer/sk_app/android/surface_glue_android.cpp
@@ -46,6 +46,7 @@
     fAndroidApp = env->NewGlobalRef(androidApp);
     jclass cls = env->GetObjectClass(fAndroidApp);
     fSetTitleMethodID = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V");
+    fSetStateMethodID = env->GetMethodID(cls, "setState", "(Ljava/lang/String;)V");
     fNativeWindow = nullptr;
     pthread_create(&fThread, nullptr, pthread_main, this);
 }
@@ -70,6 +71,12 @@
     fPThreadEnv->DeleteLocalRef(titleString);
 }
 
+void SkiaAndroidApp::setUIState(const Json::Value& state) const {
+    jstring jstr = fPThreadEnv->NewStringUTF(state.toStyledString().c_str());
+    fPThreadEnv->CallVoidMethod(fAndroidApp, fSetStateMethodID, jstr);
+    fPThreadEnv->DeleteLocalRef(jstr);
+}
+
 void SkiaAndroidApp::postMessage(const Message& message) const {
     SkDEBUGCODE(auto writeSize =) write(fPipes[1], &message, sizeof(message));
     SkASSERT(writeSize == sizeof(message));
@@ -139,6 +146,12 @@
                                              message.fTouchY);
             break;
         }
+        case kUIStateChanged: {
+            skiaAndroidApp->fWindow->onUIStateChanged(*message.stateName, *message.stateValue);
+            delete message.stateName;
+            delete message.stateValue;
+            break;
+        }
         default: {
             // do nothing
         }
@@ -229,4 +242,17 @@
     skiaAndroidApp->postMessage(message);
 }
 
+extern "C" JNIEXPORT void JNICALL Java_org_skia_viewer_ViewerActivity_onUIStateChanged(
+    JNIEnv* env, jobject activity, jlong handle, jstring stateName, jstring stateValue) {
+    auto skiaAndroidApp = (SkiaAndroidApp*)handle;
+    Message message(kUIStateChanged);
+    const char* nameChars = env->GetStringUTFChars(stateName, nullptr);
+    const char* valueChars = env->GetStringUTFChars(stateValue, nullptr);
+    message.stateName = new SkString(nameChars);
+    message.stateValue = new SkString(valueChars);
+    skiaAndroidApp->postMessage(message);
+    env->ReleaseStringUTFChars(stateName, nameChars);
+    env->ReleaseStringUTFChars(stateValue, valueChars);
+}
+
 }  // namespace sk_app
diff --git a/tools/viewer/sk_app/android/surface_glue_android.h b/tools/viewer/sk_app/android/surface_glue_android.h
index a469833..3bbf3af 100644
--- a/tools/viewer/sk_app/android/surface_glue_android.h
+++ b/tools/viewer/sk_app/android/surface_glue_android.h
@@ -12,6 +12,8 @@
 
 #include <android/native_window_jni.h>
 
+#include "SkString.h"
+
 #include "../Application.h"
 #include "../Window.h"
 
@@ -25,7 +27,8 @@
     kDestroyApp,
     kContentInvalidated,
     kKeyPressed,
-    kTouched
+    kTouched,
+    kUIStateChanged,
 };
 
 struct Message {
@@ -35,6 +38,9 @@
     int fTouchOwner, fTouchState;
     float fTouchX, fTouchY;
 
+    SkString* stateName;
+    SkString* stateValue;
+
     Message() {}
     Message(MessageType t) : fType(t) {}
 };
@@ -49,8 +55,9 @@
     void postMessage(const Message& message) const;
     void readMessage(Message* message) const;
 
-    // This must be called in SkiaAndroidApp's own pthread because the JNIEnv is thread sensitive
+    // These must be called in SkiaAndroidApp's own pthread because the JNIEnv is thread sensitive
     void setTitle(const char* title) const;
+    void setUIState(const Json::Value& state) const;
 
 private:
     pthread_t fThread;
@@ -58,7 +65,7 @@
     int fPipes[2];  // 0 is the read message pipe, 1 is the write message pipe
     JavaVM* fJavaVM;
     JNIEnv* fPThreadEnv;
-    jmethodID fSetTitleMethodID;
+    jmethodID fSetTitleMethodID, fSetStateMethodID;
 
     // This must be called in SkiaAndroidApp's own pthread because the JNIEnv is thread sensitive
     ~SkiaAndroidApp();