From 8f20c3e9d3f264676a801751a87ccd5d37e45d34 Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Wed, 8 Jul 2015 10:58:53 -0600 Subject: [PATCH] app, cmd/gomobile: subclass NativeActivity Subclassing NativeActivity makes two things possible. Firstly, we can implement an InputConnection to offer good support for IMEs, necessary for good keyboard support. Secondly, we can use it to overlay WebViews onto the NativeActivity. But to sublcass NativeActivity, we need to compile Java. To keep the toolchain go gettable, this is done with go generate. While here, check the exception after FindClass. Apparently it can throw an exception. Updates golang/go#9361. Updates golang/go#10247. Change-Id: I672545997f0c9a7580f06988a273c03404772247 Reviewed-on: https://go-review.googlesource.com/11980 Reviewed-by: Hyang-Ah Hana Kim --- app/GoNativeActivity.java | 51 ++++++++++ app/android.c | 97 +++++-------------- app/android.go | 2 + cmd/gomobile/build.go | 2 + cmd/gomobile/build_androidapp.go | 14 +++ cmd/gomobile/dex.go | 44 +++++++++ cmd/gomobile/gendex.go | 157 +++++++++++++++++++++++++++++++ cmd/gomobile/manifest.go | 4 +- cmd/gomobile/writer.go | 20 +++- 9 files changed, 315 insertions(+), 76 deletions(-) create mode 100644 app/GoNativeActivity.java create mode 100644 cmd/gomobile/dex.go create mode 100644 cmd/gomobile/gendex.go diff --git a/app/GoNativeActivity.java b/app/GoNativeActivity.java new file mode 100644 index 0000000..4a1234b --- /dev/null +++ b/app/GoNativeActivity.java @@ -0,0 +1,51 @@ +package org.golang.app; + +import android.app.Activity; +import android.app.NativeActivity; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; + +public class GoNativeActivity extends NativeActivity { + private static GoNativeActivity goNativeActivity; + + public GoNativeActivity() { + super(); + goNativeActivity = this; + } + + String getTmpdir() { + return getCacheDir().getAbsolutePath(); + } + + private void load() { + // Interestingly, NativeActivity uses a different method + // to find native code to execute, avoiding + // System.loadLibrary. The result is Java methods + // implemented in C with JNIEXPORT (and JNI_OnLoad) are not + // available unless an explicit call to System.loadLibrary + // is done. So we do it here, borrowing the name of the + // library from the same AndroidManifest.xml metadata used + // by NativeActivity. + try { + ActivityInfo ai = getPackageManager().getActivityInfo( + getIntent().getComponent(), PackageManager.GET_META_DATA); + if (ai.metaData == null) { + Log.e("Go", "loadLibrary: no manifest metadata found"); + return; + } + String libName = ai.metaData.getString("android.app.lib_name"); + System.loadLibrary(libName); + } catch (Exception e) { + Log.e("Go", "loadLibrary failed", e); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + load(); + super.onCreate(savedInstanceState); + } +} diff --git a/app/android.c b/app/android.c index 6f7b6e2..cd4dfd0 100644 --- a/app/android.c +++ b/app/android.c @@ -21,6 +21,7 @@ static jclass find_class(JNIEnv *env, const char *class_name) { jclass clazz = (*env)->FindClass(env, class_name); if (clazz == NULL) { + (*env)->ExceptionClear(env); LOG_FATAL("cannot find %s", class_name); return NULL; } @@ -30,6 +31,7 @@ static jclass find_class(JNIEnv *env, const char *class_name) { static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { jmethodID m = (*env)->GetMethodID(env, clazz, name, sig); if (m == 0) { + (*env)->ExceptionClear(env); LOG_FATAL("cannot find method %s %s", name, sig); return 0; } @@ -42,92 +44,41 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { return -1; } + // Load classes here, which uses the correct ClassLoader. current_vm = vm; current_ctx = NULL; + current_ctx_clazz = find_class(env, "org/golang/app/GoNativeActivity"); + current_ctx_clazz = (jclass)(*env)->NewGlobalRef(env, current_ctx_clazz); return JNI_VERSION_1_6; } -static void init_from_context() { - if (current_ctx == NULL) { - return; +// 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. +void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) { + JNIEnv* env = activity->env; + + // Note that activity->clazz is mis-named. + current_vm = activity->vm; + current_ctx = (*env)->NewGlobalRef(env, activity->clazz); + + // Set TMPDIR. + jmethodID gettmpdir = find_method(env, current_ctx_clazz, "getTmpdir", "()Ljava/lang/String;"); + jstring jpath = (jstring)(*env)->CallObjectMethod(env, current_ctx, gettmpdir, NULL); + const char* tmpdir = (*env)->GetStringUTFChars(env, jpath, NULL); + if (setenv("TMPDIR", tmpdir, 1) != 0) { + LOG_INFO("setenv(\"TMPDIR\", \"%s\", 1) failed: %d", tmpdir, errno); } + (*env)->ReleaseStringUTFChars(env, jpath, tmpdir); - int attached = 0; - JNIEnv* env; - switch ((*current_vm)->GetEnv(current_vm, (void**)&env, JNI_VERSION_1_6)) { - case JNI_OK: - break; - case JNI_EDETACHED: - if ((*current_vm)->AttachCurrentThread(current_vm, &env, 0) != 0) { - LOG_FATAL("cannot attach JVM"); - } - attached = 1; - break; - case JNI_EVERSION: - LOG_FATAL("bad JNI version"); - } - - // String path = context.getCacheDir().getAbsolutePath(); - jclass context_clazz = find_class(env, "android/content/Context"); - jmethodID getcachedir = find_method(env, context_clazz, "getCacheDir", "()Ljava/io/File;"); - jobject file = (*env)->CallObjectMethod(env, current_ctx, getcachedir, NULL); - jclass file_clazz = find_class(env, "java/io/File"); - jmethodID getabsolutepath = find_method(env, file_clazz, "getAbsolutePath", "()Ljava/lang/String;"); - jstring jpath = (jstring)(*env)->CallObjectMethod(env, file, getabsolutepath, NULL); - const char* path = (*env)->GetStringUTFChars(env, jpath, NULL); - if (setenv("TMPDIR", path, 1) != 0) { - LOG_INFO("setenv(\"TMPDIR\", \"%s\", 1) failed: %d", path, errno); - } - (*env)->ReleaseStringUTFChars(env, jpath, path); - - if (attached) { - (*current_vm)->DetachCurrentThread(current_vm); - } -} - -// has_prefix_key returns 1 if s starts with prefix. -static int has_prefix(const char *s, const char* prefix) { - while (*prefix) { - if (*prefix++ != *s++) - return 0; - } - return 1; -} - -// getenv_raw searches environ for name prefix and returns the string pair. -// For example, getenv_raw("PATH=") returns "PATH=/bin". -// If no entry is found, the name prefix is returned. For example "PATH=". -static const char* getenv_raw(const char *name) { - extern char** environ; - char** env = environ; - - for (env = environ; *env; env++) { - if (has_prefix(*env, name)) { - return *env; - } - } - return name; -} - -static void* call_main_and_wait() { - init_from_context(); + // Call the Go main.main. uintptr_t mainPC = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main"); if (!mainPC) { LOG_FATAL("missing main.main"); } callMain(mainPC); -} - -// Entry point from 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. -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); - call_main_and_wait(); // These functions match the methods on Activity, described at // http://developer.android.com/reference/android/app/Activity.html diff --git a/app/android.go b/app/android.go index 6290c88..8e18c49 100644 --- a/app/android.go +++ b/app/android.go @@ -45,6 +45,8 @@ JavaVM* current_vm; // current_ctx is Android's android.context.Context. May be NULL. jobject current_ctx; +jclass current_ctx_clazz; + jclass app_find_class(JNIEnv* env, const char* name); */ import "C" diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go index f4a2401..ba0ffcb 100644 --- a/cmd/gomobile/build.go +++ b/cmd/gomobile/build.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:generate go run gendex.go -o dex.go + package main import ( diff --git a/cmd/gomobile/build_androidapp.go b/cmd/gomobile/build_androidapp.go index e366abe..7fcbe15 100644 --- a/cmd/gomobile/build_androidapp.go +++ b/cmd/gomobile/build_androidapp.go @@ -7,12 +7,14 @@ package main import ( "bytes" "crypto/x509" + "encoding/base64" "encoding/pem" "errors" "fmt" "go/build" "io" "io/ioutil" + "log" "os" "path" "path/filepath" @@ -109,6 +111,18 @@ func goAndroidBuild(pkg *build.Package) error { return err } + w, err = apkwcreate("classes.dex") + if err != nil { + return err + } + dexData, err := base64.StdEncoding.DecodeString(dexStr) + if err != nil { + log.Fatal("internal error bad dexStr: %v", err) + } + if _, err := w.Write(dexData); err != nil { + return err + } + w, err = apkwcreate("lib/armeabi/lib" + libName + ".so") if err != nil { return err diff --git a/cmd/gomobile/dex.go b/cmd/gomobile/dex.go new file mode 100644 index 0000000..b1a191a --- /dev/null +++ b/cmd/gomobile/dex.go @@ -0,0 +1,44 @@ +// 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. + +// File is automatically generated by gendex.go. DO NOT EDIT. + +package main + +var dexStr = `ZGV4CjAzNQBgMPcbh5xnMiJMhJmv7LSwWLsd8n8QqVIUBwAAcAAAAHhWNBIAAAAAAAAAAH` + + `QGAAApAAAAcAAAAA8AAAAUAQAADAAAAFABAAACAAAA4AEAABAAAADwAQAAAQAAAHACAACE` + + `BAAAkAIAAJ4DAACmAwAAqgMAAMEDAADEAwAAyQMAAM8DAADSAwAA1gMAANsDAAD5AwAAGg` + + `QAADQEAABXBAAAfAQAAJEEAAClBAAAtQQAAMwEAADgBAAA9AQAAAsFAAAuBQAAMQUAADUF` + + `AABLBQAATgUAAF8FAABwBQAAfQUAAIsFAACWBQAAqQUAALQFAAC/BQAA0QUAANcFAADkBQ` + + `AA+AUAACEGAAArBgAAAwAAAAkAAAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAQAAAAEQAA` + + `ABIAAAATAAAAFAAAABUAAAAWAAAABAAAAAAAAAB0AwAABQAAAAAAAAB8AwAABgAAAAIAAA` + + `AAAAAABgAAAAMAAAAAAAAACAAAAAQAAACIAwAABgAAAAUAAAAAAAAABgAAAAgAAAAAAAAA` + + `BgAAAAoAAAAAAAAABwAAAAoAAACQAwAAFgAAAA4AAAAAAAAAFwAAAA4AAACYAwAAFwAAAA` + + `4AAACQAwAABAAGACcAAAANAA0AIgAAAAEACQAAAAAAAQAKACgAAAADAAIAHQAAAAUABAAb` + + `AAAABgAIACAAAAAHAAAAGQAAAAcAAQAZAAAACAAHABoAAAALAAsAJAAAAA0ACQAAAAAADQ` + + `AGABwAAAANAAMAHgAAAA0ABQAfAAAADQAHACEAAAANAAkAIwAAAA0ACgAoAAAADQAAAAEA` + + `AAABAAAAAAAAAAIAAAAAAAAAWQYAAAAAAAABAAEAAQAAADUGAAAGAAAAcBAAAAAAaQABAA` + + `4ABAABAAMAAQA8BgAAMwAAAG4QDAADAAwAbhALAAMADAFuEAIAAQAMARMCgABuMAMAEAIM` + + `AFQBAAA5AQoAGgABABoBJgBxIAUAEAAOAFQAAAAaARgAbiAEABAADABxEAgAAAAo9A0AGg` + + `EBABoCJQBxMAYAIQAo6wAAAAAAACkAAQABAQkqAgABAAEAAABMBgAACQAAAG4QCgABAAwA` + + `bhAHAAAADAARAAAAAgACAAIAAABRBgAABwAAAHAQDgAAAG8gAQAQAA4AAAACAAAACgAKAA` + + `MAAAAKAAoADAAAAAIAAAACAAAAAQAAAAoAAAABAAAABgAGPGluaXQ+AAJHbwAVR29OYXRp` + + `dmVBY3Rpdml0eS5qYXZhAAFJAANJTEwABElMTEwAAUwAAkxMAANMTEkAHExhbmRyb2lkL2` + + `FwcC9OYXRpdmVBY3Rpdml0eTsAH0xhbmRyb2lkL2NvbnRlbnQvQ29tcG9uZW50TmFtZTsA` + + `GExhbmRyb2lkL2NvbnRlbnQvSW50ZW50OwAhTGFuZHJvaWQvY29udGVudC9wbS9BY3Rpdm` + + `l0eUluZm87ACNMYW5kcm9pZC9jb250ZW50L3BtL1BhY2thZ2VNYW5hZ2VyOwATTGFuZHJv` + + `aWQvb3MvQnVuZGxlOwASTGFuZHJvaWQvdXRpbC9Mb2c7AA5MamF2YS9pby9GaWxlOwAVTG` + + `phdmEvbGFuZy9FeGNlcHRpb247ABJMamF2YS9sYW5nL1N0cmluZzsAEkxqYXZhL2xhbmcv` + + `U3lzdGVtOwAVTGphdmEvbGFuZy9UaHJvd2FibGU7ACFMb3JnL2dvbGFuZy9hcHAvR29OYX` + + `RpdmVBY3Rpdml0eTsAAVYAAlZMABRhbmRyb2lkLmFwcC5saWJfbmFtZQABZQAPZ2V0QWJz` + + `b2x1dGVQYXRoAA9nZXRBY3Rpdml0eUluZm8AC2dldENhY2hlRGlyAAxnZXRDb21wb25lbn` + + `QACWdldEludGVudAARZ2V0UGFja2FnZU1hbmFnZXIACWdldFN0cmluZwAJZ2V0VG1wZGly` + + `ABBnb05hdGl2ZUFjdGl2aXR5AARsb2FkAAtsb2FkTGlicmFyeQASbG9hZExpYnJhcnkgZm` + + `FpbGVkACdsb2FkTGlicmFyeTogbm8gbWFuaWZlc3QgbWV0YWRhdGEgZm91bmQACG1ldGFE` + + `YXRhAAhvbkNyZWF0ZQAPAAcOPC0AIQAHDgESEEt/Ansdh0seABQABw4AMAEABw48PAABAA` + + `ICAQoJgYAEkAUFAqwFDQCwBgIB1AYAAAANAAAAAAAAAAEAAAAAAAAAAQAAACkAAABwAAAA` + + `AgAAAA8AAAAUAQAAAwAAAAwAAABQAQAABAAAAAIAAADgAQAABQAAABAAAADwAQAABgAAAA` + + `EAAABwAgAAASAAAAQAAACQAgAAARAAAAUAAAB0AwAAAiAAACkAAACeAwAAAyAAAAQAAAA1` + + `BgAAACAAAAEAAABZBgAAABAAAAEAAAB0BgAA` + + `` diff --git a/cmd/gomobile/gendex.go b/cmd/gomobile/gendex.go new file mode 100644 index 0000000..eb90d40 --- /dev/null +++ b/cmd/gomobile/gendex.go @@ -0,0 +1,157 @@ +// 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. + +// +build ignore + +// Gendex generates a dex file used by Go apps created with gomobile. +// +// The dex is a thin extension of NativeActivity, providing access to +// a few platform features (not the SDK UI) not easily accessible from +// NDK headers. Long term these could be made part of the standard NDK, +// however that would limit gomobile to working with newer versions of +// the Android OS, so we do this while we wait. +// +// Requires ANDROID_HOME be set to the path of the Android SDK, and +// javac must be on the PATH. +package main + +import ( + "bytes" + "encoding/base64" + "errors" + "flag" + "fmt" + "go/format" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" +) + +var outfile = flag.String("o", "", "result will be written file") + +var tmpdir string + +func main() { + flag.Parse() + + var err error + tmpdir, err = ioutil.TempDir("", "gendex-") + if err != nil { + log.Fatal(err) + } + + err = gendex() + os.RemoveAll(tmpdir) + if err != nil { + log.Fatal(err) + } +} + +func gendex() error { + androidHome := os.Getenv("ANDROID_HOME") + if androidHome == "" { + return errors.New("ANDROID_HOME not set") + } + if err := os.MkdirAll(tmpdir+"/work/org/golang/app", 0775); err != nil { + return err + } + javaFiles, err := filepath.Glob("../../app/*.java") + if err != nil { + return err + } + if len(javaFiles) == 0 { + return errors.New("could not find ../../app/*.java files") + } + platform, err := findLast(androidHome + "/platforms") + if err != nil { + return err + } + cmd := exec.Command( + "javac", + "-source", "1.7", + "-target", "1.7", + "-bootclasspath", platform+"/android.jar", + "-d", tmpdir+"/work", + ) + cmd.Args = append(cmd.Args, javaFiles...) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Println(cmd.Args) + os.Stderr.Write(out) + return err + } + buildTools, err := findLast(androidHome + "/build-tools") + if err != nil { + return err + } + cmd = exec.Command( + buildTools+"/dx", + "--dex", + "--output="+tmpdir+"/classes.dex", + tmpdir+"/work", + ) + if out, err := cmd.CombinedOutput(); err != nil { + os.Stderr.Write(out) + return err + } + src, err := ioutil.ReadFile(tmpdir + "/classes.dex") + if err != nil { + return err + } + data := base64.StdEncoding.EncodeToString(src) + + buf := new(bytes.Buffer) + fmt.Fprint(buf, header) + + var piece string + for len(data) > 0 { + l := 70 + if l > len(data) { + l = len(data) + } + piece, data = data[:l], data[l:] + fmt.Fprintf(buf, "\t`%s` + \n", piece) + } + fmt.Fprintf(buf, "\t``") + out, err := format.Source(buf.Bytes()) + if err != nil { + buf.WriteTo(os.Stderr) + return err + } + + w, err := os.Create(*outfile) + if err != nil { + return err + } + if _, err := w.Write(out); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + return nil +} + +func findLast(path string) (string, error) { + dir, err := os.Open(path) + if err != nil { + return "", err + } + children, err := dir.Readdirnames(-1) + if err != nil { + return "", err + } + return path + "/" + children[len(children)-1], nil +} + +var header = `// 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. + +// File is automatically generated by gendex.go. DO NOT EDIT. + +package main + +var dexStr = ` diff --git a/cmd/gomobile/manifest.go b/cmd/gomobile/manifest.go index 04384e2..5ab308b 100644 --- a/cmd/gomobile/manifest.go +++ b/cmd/gomobile/manifest.go @@ -62,8 +62,8 @@ var manifestTmpl = template.Must(template.New("manifest").Parse(` android:versionName="1.0"> - - + diff --git a/cmd/gomobile/writer.go b/cmd/gomobile/writer.go index 3da75c8..6224f27 100644 --- a/cmd/gomobile/writer.go +++ b/cmd/gomobile/writer.go @@ -149,8 +149,20 @@ func (w *Writer) Close() error { return fmt.Errorf("apk: %v", err) } + hasDex := false + for _, entry := range w.manifest { + if entry.name == "classes.dex" { + hasDex = true + break + } + } + manifest := new(bytes.Buffer) - fmt.Fprint(manifest, manifestHeader) + if hasDex { + fmt.Fprint(manifest, manifestDexHeader) + } else { + fmt.Fprint(manifest, manifestHeader) + } certBody := new(bytes.Buffer) for _, entry := range w.manifest { @@ -206,6 +218,12 @@ Created-By: 1.0 (Go) ` +const manifestDexHeader = `Manifest-Version: 1.0 +Dex-Location: classes.dex +Created-By: 1.0 (Go) + +` + const certHeader = `Signature-Version: 1.0 Created-By: 1.0 (Go) `