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) `