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