2
0
mirror of synced 2025-02-23 06:48:15 +00:00

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 <hyangah@gmail.com>
This commit is contained in:
David Crawshaw 2015-07-08 10:58:53 -06:00
parent e5193c59b3
commit 8f20c3e9d3
9 changed files with 315 additions and 76 deletions

51
app/GoNativeActivity.java Normal file
View File

@ -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);
}
}

View File

@ -21,6 +21,7 @@
static jclass find_class(JNIEnv *env, const char *class_name) { static jclass find_class(JNIEnv *env, const char *class_name) {
jclass clazz = (*env)->FindClass(env, class_name); jclass clazz = (*env)->FindClass(env, class_name);
if (clazz == NULL) { if (clazz == NULL) {
(*env)->ExceptionClear(env);
LOG_FATAL("cannot find %s", class_name); LOG_FATAL("cannot find %s", class_name);
return NULL; 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) { static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
jmethodID m = (*env)->GetMethodID(env, clazz, name, sig); jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
if (m == 0) { if (m == 0) {
(*env)->ExceptionClear(env);
LOG_FATAL("cannot find method %s %s", name, sig); LOG_FATAL("cannot find method %s %s", name, sig);
return 0; return 0;
} }
@ -42,92 +44,41 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
return -1; return -1;
} }
// Load classes here, which uses the correct ClassLoader.
current_vm = vm; current_vm = vm;
current_ctx = NULL; 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; return JNI_VERSION_1_6;
} }
static void init_from_context() { // Entry point from our subclassed NativeActivity.
if (current_ctx == NULL) { //
return; // 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; // Call the Go main.main.
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();
uintptr_t mainPC = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main"); uintptr_t mainPC = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main");
if (!mainPC) { if (!mainPC) {
LOG_FATAL("missing main.main"); LOG_FATAL("missing main.main");
} }
callMain(mainPC); 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 // These functions match the methods on Activity, described at
// http://developer.android.com/reference/android/app/Activity.html // http://developer.android.com/reference/android/app/Activity.html

View File

@ -45,6 +45,8 @@ JavaVM* current_vm;
// current_ctx is Android's android.context.Context. May be NULL. // current_ctx is Android's android.context.Context. May be NULL.
jobject current_ctx; jobject current_ctx;
jclass current_ctx_clazz;
jclass app_find_class(JNIEnv* env, const char* name); jclass app_find_class(JNIEnv* env, const char* name);
*/ */
import "C" import "C"

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:generate go run gendex.go -o dex.go
package main package main
import ( import (

View File

@ -7,12 +7,14 @@ package main
import ( import (
"bytes" "bytes"
"crypto/x509" "crypto/x509"
"encoding/base64"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"go/build" "go/build"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -109,6 +111,18 @@ func goAndroidBuild(pkg *build.Package) error {
return err 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") w, err = apkwcreate("lib/armeabi/lib" + libName + ".so")
if err != nil { if err != nil {
return err return err

44
cmd/gomobile/dex.go Normal file
View File

@ -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` +
``

157
cmd/gomobile/gendex.go Normal file
View File

@ -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 = `

View File

@ -62,8 +62,8 @@ var manifestTmpl = template.Must(template.New("manifest").Parse(`
android:versionName="1.0"> android:versionName="1.0">
<uses-sdk android:minSdkVersion="9" /> <uses-sdk android:minSdkVersion="9" />
<application android:label="{{.Name}}" android:hasCode="false" android:debuggable="true"> <application android:label="{{.Name}}" android:debuggable="true">
<activity android:name="android.app.NativeActivity" <activity android:name="org.golang.app.GoNativeActivity"
android:label="{{.Name}}" android:label="{{.Name}}"
android:configChanges="orientation|keyboardHidden"> android:configChanges="orientation|keyboardHidden">
<meta-data android:name="android.app.lib_name" android:value="{{.LibName}}" /> <meta-data android:name="android.app.lib_name" android:value="{{.LibName}}" />

View File

@ -149,8 +149,20 @@ func (w *Writer) Close() error {
return fmt.Errorf("apk: %v", err) 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) manifest := new(bytes.Buffer)
fmt.Fprint(manifest, manifestHeader) if hasDex {
fmt.Fprint(manifest, manifestDexHeader)
} else {
fmt.Fprint(manifest, manifestHeader)
}
certBody := new(bytes.Buffer) certBody := new(bytes.Buffer)
for _, entry := range w.manifest { 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 const certHeader = `Signature-Version: 1.0
Created-By: 1.0 (Go) Created-By: 1.0 (Go)
` `