diff --git a/platform_tools/android/apps/viewer/build.gradle b/platform_tools/android/apps/viewer/build.gradle index 79cac8a7d4..15cff2cb39 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 0000000000..4c9dcd4b84 --- /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 optionList = new ArrayList<>(); + String[] optionStrings = new String[options.length()]; + for(int i=0; i(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 49f711d517..ce5bb0deda 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.Surface; 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 @@ public class ViewerActivity 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 @@ public class ViewerActivity @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 @@ public class ViewerActivity 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 @@ public class ViewerActivity } 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 4b890bd5e1..ee1695afd1 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 @@ import android.app.Application; 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 class ViewerApplication extends Application { } 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 6597a48dcc..985b67d085 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 @@ - - + + + tools:context=".ViewerActivity"> + + + + + + + + - 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 0000000000..7a7d539d43 --- /dev/null +++ b/platform_tools/android/apps/viewer/src/main/res/layout/state_item.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + 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 0000000000..582c566679 --- /dev/null +++ b/platform_tools/android/apps/viewer/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Open navigation drawer + Close navigation drawer + \ No newline at end of file diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp index c4b8b26dc5..6519a3c8be 100644 --- a/tools/viewer/Viewer.cpp +++ b/tools/viewer/Viewer.cpp @@ -35,6 +35,12 @@ static bool on_touch_handler(int owner, Window::InputState state, float x, float return viewer->onTouch(owner, state, x, y); } +static void on_ui_state_changed_handler(const SkString& stateName, const SkString& stateValue, void* userData) { + Viewer* viewer = reinterpret_cast(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, @@ -53,6 +59,12 @@ const char *kBackendTypeStrings[sk_app::Window::kBackendTypeCount] = { " [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) @@ -79,6 +91,7 @@ Viewer::Viewer(int argc, char** argv, void* platformData) 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]() { @@ -213,6 +226,10 @@ void Viewer::updateTitle() { } void Viewer::setupCurrentSlide(int previousSlide) { + if (fCurrentSlide == previousSlide) { + return; // no change; do nothing + } + fGesture.reset(); fDefaultMatrix.reset(); fDefaultMatrixInv.reset(); @@ -235,6 +252,7 @@ void Viewer::setupCurrentSlide(int previousSlide) { } this->updateTitle(); + this->updateUIState(); fSlides[fCurrentSlide]->load(); if (previousSlide >= 0) { fSlides[previousSlide]->unload(); @@ -382,3 +400,49 @@ void Viewer::onIdle(double ms) { 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 0bafee175b..e752b3f939 100644 --- a/tools/viewer/Viewer.h +++ b/tools/viewer/Viewer.h @@ -25,12 +25,15 @@ public: 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 b78f39aa37..a02e993d21 100644 --- a/tools/viewer/sk_app/Window.cpp +++ b/tools/viewer/sk_app/Window.cpp @@ -32,12 +32,16 @@ static bool default_touch_func(int owner, Window::InputState state, float x, flo 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 @@ bool Window::onTouch(int owner, InputState state, float x, float y) { 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 backbuffer = fWindowContext->getBackbufferSurface(); diff --git a/tools/viewer/sk_app/Window.h b/tools/viewer/sk_app/Window.h index 65e1542e2c..9ca4231c80 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ protected: 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 ed03c814dd..4f33870c1a 100644 --- a/tools/viewer/sk_app/android/Window_android.cpp +++ b/tools/viewer/sk_app/android/Window_android.cpp @@ -41,6 +41,10 @@ void Window_android::setTitle(const char* title) { 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 f7d348bc91..f1c079835b 100644 --- a/tools/viewer/sk_app/android/Window_android.h +++ b/tools/viewer/sk_app/android/Window_android.h @@ -28,6 +28,7 @@ public: 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 958b7876b8..daf26a3852 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 @@ SkiaAndroidApp::SkiaAndroidApp(JNIEnv* env, jobject androidApp) { 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 @@ void SkiaAndroidApp::setTitle(const char* title) const { 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 @@ int SkiaAndroidApp::message_callback(int fd, int events, void* data) { 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 @@ extern "C" JNIEXPORT void JNICALL Java_org_skia_viewer_ViewerActivity_onTouched( 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 a4698336f0..3bbf3af9c3 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 +#include "SkString.h" + #include "../Application.h" #include "../Window.h" @@ -25,7 +27,8 @@ enum MessageType { kDestroyApp, kContentInvalidated, kKeyPressed, - kTouched + kTouched, + kUIStateChanged, }; struct Message { @@ -35,6 +38,9 @@ struct Message { int fTouchOwner, fTouchState; float fTouchX, fTouchY; + SkString* stateName; + SkString* stateValue; + Message() {} Message(MessageType t) : fType(t) {} }; @@ -49,8 +55,9 @@ struct SkiaAndroidApp { 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 @@ private: 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();