app: android activity lifecycle awareness

This CL fixes two bugs in the existing app implementation on android.
The first, is it assumed a single NativeActivity instance is created
per process. This is not true. If you open an app, hit the back
button, then open it again, the original activity is destroyed and a
new one is created. So only call main.main in the first onCreate.

The second bug has to do with window lifetimes. Previously we only
processed GL work while the window existed, as part of a paint cycle.
This missed GL events called as part of a lifecycle downgrade when
the window was destroyed. (I.e. the contents of onStop.) This CL
fixes this by making the main android event processing loop last for
the life of the process, not the window.

Fixes golang/go#11804.

Change-Id: Ia03e464aab5bc10ba75564b7ca11054515cda011
Reviewed-on: https://go-review.googlesource.com/12533
Reviewed-by: Nigel Tao <nigeltao@golang.org>
This commit is contained in:
David Crawshaw 2015-07-22 15:19:43 -04:00
Родитель 723de9f813
Коммит 5c91e60c93
4 изменённых файлов: 163 добавлений и 89 удалений

Просмотреть файл

@ -51,11 +51,18 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
return JNI_VERSION_1_6;
}
int main_running = 0;
// Entry point from our subclassed NativeActivity.
//
// By here, the Go runtime has been initialized (as we are running in
// -buildmode=c-shared) but main.main hasn't been called yet.
// -buildmode=c-shared) but the first time it is called, Go's main.main
// hasn't been called yet.
//
// The Activity may be created and destroyed multiple times throughout
// the life of a single process. Each time, onCreate is called.
void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) {
if (!main_running) {
JNIEnv* env = activity->env;
// Note that activity->clazz is mis-named.
@ -79,9 +86,14 @@ void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_
LOG_FATAL("missing main.main");
}
callMain(mainPC);
main_running = 1;
}
// These functions match the methods on Activity, described at
// http://developer.android.com/reference/android/app/Activity.html
//
// Note that onNativeWindowResized is not called on resize. Avoid it.
// https://code.google.com/p/android/issues/detail?id=180645
activity->callbacks->onStart = onStart;
activity->callbacks->onResume = onResume;
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
@ -97,7 +109,5 @@ void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
activity->callbacks->onLowMemory = onLowMemory;
// Note that onNativeWindowResized is not called on resize. Avoid it.
// https://code.google.com/p/android/issues/detail?id=180645
onCreate(activity);
}

Просмотреть файл

@ -42,7 +42,6 @@ import "C"
import (
"log"
"os"
"runtime"
"time"
"unsafe"
@ -79,12 +78,6 @@ func callMain(mainPC uintptr) {
go callfn.CallFn(mainPC)
}
//export onCreate
func onCreate(activity *C.ANativeActivity) {
config := windowConfigRead(activity)
pixelsPerPt = config.pixelsPerPt
}
//export onStart
func onStart(activity *C.ANativeActivity) {
}
@ -106,6 +99,16 @@ func onPause(activity *C.ANativeActivity) {
func onStop(activity *C.ANativeActivity) {
}
//export onCreate
func onCreate(activity *C.ANativeActivity) {
// Set the initial configuration.
//
// Note we use unbuffered channels to talk to the activity loop, and
// NativeActivity calls these callbacks sequentially, so configuration
// will be set before <-windowRedrawNeeded is processed.
windowConfigChange <- windowConfigRead(activity)
}
//export onDestroy
func onDestroy(activity *C.ANativeActivity) {
}
@ -132,19 +135,17 @@ func onNativeWindowRedrawNeeded(activity *C.ANativeActivity, window *C.ANativeWi
//export onNativeWindowDestroyed
func onNativeWindowDestroyed(activity *C.ANativeActivity, window *C.ANativeWindow) {
windowDestroyed <- true
windowDestroyed <- window
}
var queue *C.AInputQueue
//export onInputQueueCreated
func onInputQueueCreated(activity *C.ANativeActivity, q *C.AInputQueue) {
queue = q
inputQueue <- q
}
//export onInputQueueDestroyed
func onInputQueueDestroyed(activity *C.ANativeActivity, q *C.AInputQueue) {
queue = nil
inputQueue <- nil
}
//export onContentRectChanged
@ -213,7 +214,8 @@ func onLowMemory(activity *C.ANativeActivity) {
}
var (
windowDestroyed = make(chan bool)
inputQueue = make(chan *C.AInputQueue)
windowDestroyed = make(chan *C.ANativeWindow)
windowCreated = make(chan *C.ANativeWindow)
windowRedrawNeeded = make(chan *C.ANativeWindow)
windowRedrawDone = make(chan struct{})
@ -223,21 +225,3 @@ var (
func init() {
registerGLViewportFilter()
}
func main(f func(App)) {
// Preserve this OS thread for the GL context created in windowDraw.
runtime.LockOSThread()
donec := make(chan struct{})
go func() {
f(app{})
close(donec)
}()
for w := range windowCreated {
if windowDraw(w, queue, donec) {
return
}
}
panic("unreachable")
}

Просмотреть файл

@ -80,11 +80,7 @@ func (app) EndPaint() {
// This enforces that the final receive (for this paint cycle) on
// gl.WorkAvailable happens before the send on endPaint.
gl.Flush()
select {
case endPaint <- struct{}{}:
default:
}
endPaint <- struct{}{}
}
var filters []func(interface{}) interface{}

Просмотреть файл

@ -5,7 +5,6 @@
package app
/*
#include <android/log.h>
#include <android/native_activity.h>
#include <android/native_window.h>
#include <android/input.h>
@ -24,57 +23,67 @@ const EGLint RGB_888[] = {
EGL_NONE
};
EGLDisplay display;
EGLSurface surface;
EGLDisplay display = NULL;
EGLSurface surface = NULL;
#define LOG_ERROR(...) __android_log_print(ANDROID_LOG_ERROR, "Go", __VA_ARGS__)
char* initEGLDisplay() {
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (!eglInitialize(display, 0, 0)) {
return "EGL initialize failed";
}
return NULL;
}
void createEGLWindow(ANativeWindow* window) {
char* createEGLSurface(ANativeWindow* window) {
char* err;
EGLint numConfigs, format;
EGLConfig config;
EGLContext context;
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (!eglInitialize(display, 0, 0)) {
LOG_ERROR("EGL initialize failed");
return;
if (display == 0) {
if ((err = initEGLDisplay()) != NULL) {
return err;
}
}
if (!eglChooseConfig(display, RGB_888, &config, 1, &numConfigs)) {
LOG_ERROR("EGL choose RGB_888 config failed");
return;
return "EGL choose RGB_888 config failed";
}
if (numConfigs <= 0) {
LOG_ERROR("EGL no config found");
return;
return "EGL no config found";
}
eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
if (ANativeWindow_setBuffersGeometry(window, 0, 0, format) != 0) {
LOG_ERROR("EGL set buffers geometry failed");
return;
return "EGL set buffers geometry failed";
}
surface = eglCreateWindowSurface(display, config, window, NULL);
if (surface == EGL_NO_SURFACE) {
LOG_ERROR("EGL create surface failed");
return;
return "EGL create surface failed";
}
const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
LOG_ERROR("eglMakeCurrent failed");
return;
return "eglMakeCurrent failed";
}
return NULL;
}
#undef LOG_ERROR
char* destroyEGLSurface() {
if (!eglDestroySurface(display, surface)) {
return "EGL destroy surface failed";
}
return NULL;
}
*/
import "C"
import (
"fmt"
"log"
"runtime"
"golang.org/x/mobile/event/config"
"golang.org/x/mobile/event/lifecycle"
@ -84,7 +93,18 @@ import (
"golang.org/x/mobile/gl"
)
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) {
func main(f func(App)) {
// Preserve this OS thread for the GL context created below.
runtime.LockOSThread()
donec := make(chan struct{})
go func() {
f(app{})
close(donec)
}()
var q *C.AInputQueue
// Android can send a windowRedrawNeeded event any time, including
// in the middle of a paint cycle. The redraw event may have changed
// the size of the screen, so any partial painting is now invalidated.
@ -97,15 +117,27 @@ func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (
// windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded
// to return.
var redrawGen, paintGen uint32
for {
processEvents(queue)
if q != nil {
processEvents(q)
}
select {
case <-windowCreated:
case q = <-inputQueue:
case <-donec:
return true
return
case cfg := <-windowConfigChange:
// TODO save orientation
pixelsPerPt = cfg.pixelsPerPt
case w := <-windowRedrawNeeded:
newWindow := C.surface == nil
if newWindow {
if errStr := C.createEGLSurface(w); errStr != nil {
log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError())
return
}
}
sendLifecycle(lifecycle.StageFocused)
widthPx := int(C.ANativeWindow_getWidth(w))
heightPx := int(C.ANativeWindow_getHeight(w))
@ -116,29 +148,44 @@ func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (
HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt),
PixelsPerPt: pixelsPerPt,
}
if paintGen == 0 {
paintGen++
C.createEGLWindow(w)
redrawGen++
if newWindow {
// New window, begin paint loop.
// TODO(crawshaw): If we get a <-windowCreated in between sending a
// paint.Event and receiving on endPaint, we can double-paint. (BUG)
paintGen = redrawGen
eventsIn <- paint.Event{}
}
redrawGen++
case <-windowDestroyed:
if C.surface != nil {
if errStr := C.destroyEGLSurface(); errStr != nil {
log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError())
return
}
}
C.surface = nil
sendLifecycle(lifecycle.StageAlive)
return false
case <-gl.WorkAvailable:
gl.DoWork()
case <-endPaint:
if paintGen == redrawGen {
if C.surface != nil {
// eglSwapBuffers blocks until vsync.
C.eglSwapBuffers(C.display, C.surface)
if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE {
log.Printf("app: failed to swap buffers (%s)", eglGetError())
}
}
select {
case windowRedrawDone <- struct{}{}:
default:
}
}
paintGen = redrawGen
if C.surface != nil {
redrawGen++
eventsIn <- paint.Event{}
}
paintGen = redrawGen
}
}
}
@ -186,3 +233,40 @@ func processEvent(e *C.AInputEvent) {
log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e))
}
}
func eglGetError() string {
switch errNum := C.eglGetError(); errNum {
case C.EGL_SUCCESS:
return "EGL_SUCCESS"
case C.EGL_NOT_INITIALIZED:
return "EGL_NOT_INITIALIZED"
case C.EGL_BAD_ACCESS:
return "EGL_BAD_ACCESS"
case C.EGL_BAD_ALLOC:
return "EGL_BAD_ALLOC"
case C.EGL_BAD_ATTRIBUTE:
return "EGL_BAD_ATTRIBUTE"
case C.EGL_BAD_CONTEXT:
return "EGL_BAD_CONTEXT"
case C.EGL_BAD_CONFIG:
return "EGL_BAD_CONFIG"
case C.EGL_BAD_CURRENT_SURFACE:
return "EGL_BAD_CURRENT_SURFACE"
case C.EGL_BAD_DISPLAY:
return "EGL_BAD_DISPLAY"
case C.EGL_BAD_SURFACE:
return "EGL_BAD_SURFACE"
case C.EGL_BAD_MATCH:
return "EGL_BAD_MATCH"
case C.EGL_BAD_PARAMETER:
return "EGL_BAD_PARAMETER"
case C.EGL_BAD_NATIVE_PIXMAP:
return "EGL_BAD_NATIVE_PIXMAP"
case C.EGL_BAD_NATIVE_WINDOW:
return "EGL_BAD_NATIVE_WINDOW"
case C.EGL_CONTEXT_LOST:
return "EGL_CONTEXT_LOST"
default:
return fmt.Sprintf("Unknown EGL err: %d", errNum)
}
}