app: introduce Config and start registration

Config provides a way to concurrently access Width and Height.

Register provides a way for packages to run code on app state change
without plumbing changes all the way to the process main function.
This is motivated by gl/glutil.Image which needs to rebuild its
textures on start/stop and can be deeply nested.
(See golang.org/cl/9707 for the followup.)

Tested manually on android and darwin/amd64. Doing this kind makes it
clear any CL modifying this code needs a lot of manual testing right
now, so some kind of trybot support is something I'm going to
prioritise.

Fixes golang/go#10686
Fixes golang/go#10461
Fixes golang/go#10442
Fixes golang/go#10226
Updates golang/go#10327

Change-Id: I2882ebf3995b6ed857cda823e94fbb17c54b43a8
Reviewed-on: https://go-review.googlesource.com/9708
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
This commit is contained in:
David Crawshaw 2015-05-05 06:52:19 -07:00
parent 601608a0e0
commit 5cddc1460e
11 changed files with 252 additions and 97 deletions

View File

@ -148,12 +148,13 @@ static void* call_main_and_wait() {
// Runtime entry point when using NativeActivity.
void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) {
// Note that activity->clazz is mis-named.
current_vm = activity->vm;
current_ctx = (*activity->env)->NewGlobalRef(activity->env, activity->clazz);
current_native_activity = activity;
call_main_and_wait();
if (current_ctx == NULL) {
// Note that activity->clazz is mis-named.
current_vm = activity->vm;
current_ctx = (*activity->env)->NewGlobalRef(activity->env, activity->clazz);
call_main_and_wait();
}
// These functions match the methods on Activity, described at
// http://developer.android.com/reference/android/app/Activity.html

View File

@ -69,8 +69,6 @@ import (
"golang.org/x/mobile/geom"
)
var running = make(chan struct{}) // closed after app.Run is called
//export callMain
func callMain(mainPC uintptr) {
for _, name := range []string{"TMPDIR", "PATH", "LD_LIBRARY_PATH"} {
@ -79,7 +77,7 @@ func callMain(mainPC uintptr) {
C.free(unsafe.Pointer(n))
}
go callfn.CallFn(mainPC)
<-running
<-mainCalled
log.Print("app.Run called")
}
@ -187,20 +185,17 @@ func onConfigurationChanged(activity *C.ANativeActivity) {
func onLowMemory(activity *C.ANativeActivity) {
}
type androidState struct {
}
func (androidState) JavaVM() unsafe.Pointer {
func (Config) JavaVM() unsafe.Pointer {
return unsafe.Pointer(C.current_vm)
}
// ClassFinder returns a C function pointer for finding a given class using
// the app class loader. (jclass) (*fn)(JNIEnv*, const char*).
func (androidState) ClassFinder() unsafe.Pointer {
func (Config) ClassFinder() unsafe.Pointer {
return unsafe.Pointer(C.app_find_class)
}
func (androidState) AndroidContext() unsafe.Pointer {
func (Config) AndroidContext() unsafe.Pointer {
return unsafe.Pointer(C.current_ctx)
}
@ -261,17 +256,9 @@ func (a *asset) Close() error {
return nil
}
func runStart(cb Callbacks) {
State = androidState{}
if cb.Start != nil {
cb.Start()
}
}
// notifyInitDone informs Java that the program is initialized.
// A NativeActivity will not create a window until this is called.
func run(cb Callbacks) {
func run(callbacks []Callbacks) {
// We want to keep the event loop on a consistent OS thread.
runtime.LockOSThread()
@ -281,12 +268,14 @@ func run(cb Callbacks) {
C.free(unsafe.Pointer(ctag))
C.free(unsafe.Pointer(cstr))
close(mainCalled)
if C.current_native_activity == nil {
runStart(cb)
close(running)
stateStart(callbacks)
// TODO: stateStop under some conditions.
select {}
} else {
close(running)
windowDrawLoop(cb, <-windowCreated, queue)
for w := range windowCreated {
windowDraw(callbacks, w, queue)
}
}
}

View File

@ -10,14 +10,20 @@ import (
"io"
"golang.org/x/mobile/event"
"golang.org/x/mobile/geom"
)
var callbacks []Callbacks
// Run starts the app.
//
// It must be called directly from the main function and will
// block until the app exits.
//
// TODO(crawshaw): Remove cb parameter.
func Run(cb Callbacks) {
run(cb)
callbacks = append(callbacks, cb)
run(callbacks)
}
// Callbacks is the set of functions called by the app.
@ -38,7 +44,7 @@ type Callbacks struct {
// Stop is called shortly before a program is suspended.
//
// When Stop is received, the app is no longer visible and not is
// When Stop is received, the app is no longer visible and is not
// receiving events. It should:
//
// - Save any state the user expects saved (for example text).
@ -63,6 +69,15 @@ type Callbacks struct {
// Touch is called by the app when a touch event occurs.
Touch func(event.Touch)
// Config is called by the app when configuration has changed.
Config func(new, old Config)
}
// Register registers a set of callbacks.
// Must be called before Run.
func Register(cb Callbacks) {
callbacks = append(callbacks, cb)
}
// Open opens a named asset.
@ -87,9 +102,22 @@ type ReadSeekCloser interface {
io.Closer
}
// State is global application-specific state.
// GetConfig returns the current application state.
// It will block until Run has been called.
func GetConfig() Config {
select {
case <-mainCalled:
default:
panic("app.GetConfig is not available before app.Run is called")
}
configCurMu.Lock()
defer configCurMu.Unlock()
return configCur
}
// Config is global application-specific configuration.
//
// The State variable also holds operating system specific state.
// The Config variable also holds operating system specific state.
// Android apps have the extra methods:
//
// // JavaVM returns a JNI *JavaVM.
@ -98,5 +126,17 @@ type ReadSeekCloser interface {
// // AndroidContext returns a jobject for the app android.context.Context.
// AndroidContext() unsafe.Pointer
//
// State is not valid until Run has been called.
var State interface{}
// These extra methods are deliberately difficult to access because they
// must be used with care. Their use implies the use of cgo, which probably
// requires you understand the initialization process in the app package.
// Also care must be taken to write both Android, iOS, and desktop-testing
// versions to maintain portability.
type Config struct {
// Width is the width of the device screen.
Width geom.Pt
// Height is the height of the device screen.
Height geom.Pt
// TODO: Orientation
}

View File

@ -52,20 +52,23 @@ func init() {
initThreadID = uint64(C.threadID())
}
func run(callbacks Callbacks) {
func run(callbacks []Callbacks) {
if tid := uint64(C.threadID()); tid != initThreadID {
log.Fatalf("app.Run called on thread %d, but app.init ran on %d", tid, initThreadID)
}
cb = callbacks
close(mainCalled)
C.runApp()
}
//export setGeom
func setGeom(pixelsPerPt float32, width, height int) {
// Macs default to 72 DPI, so scales are equivalent.
geom.PixelsPerPt = pixelsPerPt
geom.Width = geom.Pt(float32(width) / pixelsPerPt)
geom.Height = geom.Pt(float32(height) / pixelsPerPt)
if geom.PixelsPerPt == 0 {
// Macs default to 72 DPI, so scales are equivalent.
geom.PixelsPerPt = pixelsPerPt
}
configAlt.Width = geom.Pt(float32(width) / geom.PixelsPerPt)
configAlt.Height = geom.Pt(float32(height) / geom.PixelsPerPt)
configSwap(callbacks)
}
func initGL() {
@ -74,12 +77,9 @@ func initGL() {
var id C.GLuint
C.glGenVertexArrays(1, &id)
C.glBindVertexArray(id)
if cb.Start != nil {
cb.Start()
}
stateStart(callbacks)
}
var cb Callbacks
var initGLOnce sync.Once
var touchEvents struct {
@ -94,7 +94,7 @@ func sendTouch(ty event.TouchType, x, y float32) {
Type: ty,
Loc: geom.Point{
X: geom.Pt(x / geom.PixelsPerPt),
Y: geom.Height - geom.Pt(y/geom.PixelsPerPt),
Y: GetConfig().Height - geom.Pt(y/geom.PixelsPerPt),
},
})
touchEvents.Unlock()
@ -124,17 +124,21 @@ func drawgl(ctx C.GLintptr) {
pending := touchEvents.pending
touchEvents.pending = nil
touchEvents.Unlock()
if cb.Touch != nil {
for _, e := range pending {
cb.Touch(e)
for _, cb := range callbacks {
if cb.Touch != nil {
for _, e := range pending {
cb.Touch(e)
}
}
}
// TODO: is the library or the app responsible for clearing the buffers?
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
if cb.Draw != nil {
cb.Draw()
for _, cb := range callbacks {
if cb.Draw != nil {
cb.Draw()
}
}
C.unlockContext(ctx)

View File

@ -110,11 +110,14 @@ func setGeom(width, height int) {
if geom.PixelsPerPt == 0 {
geom.PixelsPerPt = float32(ppi()) / 72
}
geom.Width = geom.Pt(float32(width) / geom.PixelsPerPt)
geom.Height = geom.Pt(float32(height) / geom.PixelsPerPt)
configAlt.Width = geom.Pt(float32(width) / geom.PixelsPerPt)
configAlt.Height = geom.Pt(float32(height) / geom.PixelsPerPt)
configSwap()
}
func initGL() {
stateStart()
}
var cb Callbacks

View File

@ -90,7 +90,7 @@ import (
"golang.org/x/mobile/gl"
)
func windowDrawLoop(cb Callbacks, w *C.ANativeWindow, queue *C.AInputQueue) {
func windowDraw(callbacks []Callbacks, w *C.ANativeWindow, queue *C.AInputQueue) {
C.createEGLWindow(w)
// TODO: is the library or the app responsible for clearing the buffers?
@ -102,51 +102,61 @@ func windowDrawLoop(cb Callbacks, w *C.ANativeWindow, queue *C.AInputQueue) {
log.Printf("GL initialization error: %s", errv)
}
geom.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt)
geom.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt)
configAlt.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt)
configAlt.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt)
configSwap(callbacks)
// Wait until geometry and GL is initialized before cb.Start.
runStart(cb)
stateStart(callbacks)
for {
processEvents(cb, queue)
processEvents(callbacks, queue)
select {
case <-windowRedrawNeeded:
// Re-query the width and height.
C.querySurfaceWidthAndHeight()
geom.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt)
geom.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt)
configAlt.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt)
configAlt.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt)
gl.Viewport(0, 0, int(C.windowWidth), int(C.windowHeight))
configSwap(callbacks)
case <-windowDestroyed:
if cb.Stop != nil {
cb.Stop()
}
stateStop(callbacks)
return
default:
if cb.Draw != nil {
cb.Draw()
for _, cb := range callbacks {
if cb.Draw != nil {
cb.Draw()
}
}
C.eglSwapBuffers(C.display, C.surface)
}
}
}
func processEvents(cb Callbacks, queue *C.AInputQueue) {
func processEvents(callbacks []Callbacks, queue *C.AInputQueue) {
var event *C.AInputEvent
for C.AInputQueue_getEvent(queue, &event) >= 0 {
if C.AInputQueue_preDispatchEvent(queue, event) != 0 {
continue
}
processEvent(cb, event)
processEvent(callbacks, event)
C.AInputQueue_finishEvent(queue, event, 0)
}
}
func processEvent(cb Callbacks, e *C.AInputEvent) {
func processEvent(callbacks []Callbacks, e *C.AInputEvent) {
switch C.AInputEvent_getType(e) {
case C.AINPUT_EVENT_TYPE_KEY:
log.Printf("TODO input event: key")
case C.AINPUT_EVENT_TYPE_MOTION:
if cb.Touch == nil {
// TODO: calculate hasTouch once in run
hasTouch := false
for _, cb := range callbacks {
if cb.Touch != nil {
hasTouch = true
}
}
if !hasTouch {
return
}
@ -165,14 +175,19 @@ func processEvent(cb Callbacks, e *C.AInputEvent) {
if i == upDownIndex {
typ = upDownTyp
}
cb.Touch(event.Touch{
t := event.Touch{
ID: event.TouchSequenceID(C.AMotionEvent_getPointerId(e, i)),
Type: typ,
Loc: geom.Point{
X: geom.Pt(float32(C.AMotionEvent_getX(e, i)) / geom.PixelsPerPt),
Y: geom.Pt(float32(C.AMotionEvent_getY(e, i)) / geom.PixelsPerPt),
},
})
}
for _, cb := range callbacks {
if cb.Touch != nil {
cb.Touch(t)
}
}
}
default:
log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e))

106
app/state.go Normal file
View File

@ -0,0 +1,106 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package app
/*
There are three notions of state at work in this package.
The first is Unix process state. Because mobile devices can be compiled
as -buildmode=c-shared and -buildmode=c-archive, there is code in this
package executed by global constructor which runs before (and even is
reponsible for triggering) the Go main function call. This is tracked
by the mainCalled channel.
The second is runState. An app may "Start" and "Stop" multiple times
over the life of the unix process. This involes the creation and
destruction of OpenGL windows and calling user Callbacks. Some user
functions must block in the stop state.
The third is Config, user-visible app configuration. It is only
available after the app has started.
*/
import (
"sync"
"golang.org/x/mobile/geom"
)
// mainCalled is closed after the Go main and app.Run functions have
// been called. This happens before an app enters the start state and
// may happen before a window is created (on android).
var mainCalled = make(chan struct{})
var (
configCurMu sync.Mutex // guards configCur pointer, not contents
configCur Config
configAlt Config // used to stage new state
)
func init() {
// Configuration is not available while the app is stopped,
// so we begin the program with configCurMu locked. It will
// be locked whenever !running.
configCurMu.Lock()
}
var (
running = false
startFuncs []func()
stopFuncs []func()
)
func stateStart(callbacks []Callbacks) {
if running {
return
}
running = true
configCurMu.Unlock() // GetConfig is now available
for _, cb := range callbacks {
if cb.Start != nil {
cb.Start()
}
}
}
func stateStop(callbacks []Callbacks) {
if !running {
return
}
running = false
configCurMu.Lock() // GetConfig is no longer available
for _, cb := range callbacks {
if cb.Stop != nil {
cb.Stop()
}
}
}
// configSwap is called to replace configCur with configAlt and if
// necessary inform the running the app. Calls to configSwap must be
// made after updating configAlt.
func configSwap(callbacks []Callbacks) {
if !running {
// configCurMu is already locked, and no-one else
// is around to look at configCur, so we modify it
// directly.
configCur = configAlt
geom.Width, geom.Height = configCur.Width, configCur.Height // TODO: remove
return
}
configCurMu.Lock()
old := configCur
configCur = configAlt
configCurMu.Unlock()
geom.Width, geom.Height = configCur.Width, configCur.Height // TODO: remove
for _, cb := range callbacks {
if cb.Config != nil {
cb.Config(configCur, old)
}
}
}

View File

@ -29,11 +29,11 @@ import (
"golang.org/x/mobile/geom"
)
var cb Callbacks
var callbacks []Callbacks
func run(callbacks Callbacks) {
func run(cbs []Callbacks) {
runtime.LockOSThread()
cb = callbacks
callbacks = cbs
C.runApp()
}
@ -41,9 +41,12 @@ func run(callbacks Callbacks) {
func onResize(w, h int) {
// TODO(nigeltao): don't assume 72 DPI. DisplayWidth / DisplayWidthMM
// is probably the best place to start looking.
geom.PixelsPerPt = 1
geom.Width = geom.Pt(w)
geom.Height = geom.Pt(h)
if geom.PixelsPerPt == 0 {
geom.PixelsPerPt = 1
}
configAlt.Width = geom.Pt(w)
configAlt.Height = geom.Pt(h)
configSwap(callbacks)
}
var touchEvents struct {
@ -79,27 +82,27 @@ func onDraw() {
pending := touchEvents.pending
touchEvents.pending = nil
touchEvents.Unlock()
if cb.Touch != nil {
for _, e := range pending {
cb.Touch(e)
for _, cb := range callbacks {
if cb.Touch != nil {
for _, e := range pending {
cb.Touch(e)
}
}
}
if cb.Draw != nil {
cb.Draw()
for _, cb := range callbacks {
if cb.Draw != nil {
cb.Draw()
}
}
}
//export onStart
func onStart() {
if cb.Start != nil {
cb.Start()
}
stateInit(callbacks)
}
//export onStop
func onStop() {
if cb.Stop != nil {
cb.Stop()
}
stateStop(callbacks)
}

View File

@ -243,7 +243,7 @@ var (
)
func initAL() {
state := app.State.(interface {
state := app.GetConfig().(interface {
JavaVM() unsafe.Pointer
AndroidContext() unsafe.Pointer
})

View File

@ -79,12 +79,8 @@ func init() {
}
func initSeq() {
vm := app.State.(interface {
JavaVM() unsafe.Pointer
}).JavaVM()
classFinder := app.State.(interface {
ClassFinder() unsafe.Pointer
}).ClassFinder()
vm := app.GetConfig().JavaVM()
classFinder := app.GetConfig().ClassFinder()
C.init_seq(vm, classFinder)
}

View File

@ -110,10 +110,8 @@ func (r Rectangle) String() string { return r.Min.String() + "-" + r.Max.String(
// Not valid until app initialization has completed.
var PixelsPerPt float32
// Width is the width of the device screen.
// Not valid until app initialization has completed.
// Width is deprecated. Use app.GetConfig().Width.
var Width Pt
// Height is the height of the device screen.
// Not valid until app initialization has completed.
// Height is deprecated. Use app.GetConfig().Height.
var Height Pt