Use Gio JVM bridge for Android sharing

This commit is contained in:
Joe Julian
2026-04-06 17:09:28 -07:00
parent d037393a16
commit fe9f3ae112
2 changed files with 63 additions and 63 deletions
+35 -59
View File
@@ -9,18 +9,6 @@ package main
#include <jni.h>
#include <stdlib.h>
static jint jni_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) {
return JNI_GetCreatedJavaVMs(vmBuf, bufLen, nVMs);
}
static jint jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) {
return (*vm)->AttachCurrentThread(vm, p_env, thr_args);
}
static jint jni_DetachCurrentThread(JavaVM *vm) {
return (*vm)->DetachCurrentThread(vm);
}
static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) {
return (*env)->GetObjectClass(env, obj);
}
@@ -72,10 +60,17 @@ import (
"unsafe"
"gioui.org/app"
_ "unsafe"
)
type androidVaultSharer struct{}
//go:linkname gioJavaVM gioui.org/app.javaVM
func gioJavaVM() *C.JavaVM
//go:linkname gioRunInJVM gioui.org/app.runInJVM
func gioRunInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv))
func newPlatformVaultSharer(goos string) vaultSharer {
return androidVaultSharer{}
}
@@ -88,55 +83,36 @@ func (androidVaultSharer) ShareVault(path, title string) error {
if C.jni_IsNull(ctx) != 0 {
return fmt.Errorf("android app context is not available")
}
vm, err := androidJavaVM()
if err != nil {
return err
}
var env *C.JNIEnv
if res := C.jni_AttachCurrentThread(vm, &env, nil); res != C.JNI_OK {
return fmt.Errorf("attach JVM thread: %d", int(res))
}
defer C.jni_DetachCurrentThread(vm)
sharerClass, err := androidLoadClass(env, ctx, "org.julianfamily.keepassgo.AndroidShare")
if err != nil {
return err
}
methodName := cString("shareVault")
defer C.free(unsafe.Pointer(methodName))
methodSig := cString("(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)V")
defer C.free(unsafe.Pointer(methodSig))
method := C.jni_GetStaticMethodID(env, sharerClass, methodName, methodSig)
if method == nil {
if err := androidJNIError(env, "resolve shareVault method"); err != nil {
return err
var callErr error
gioRunInJVM(gioJavaVM(), func(env *C.JNIEnv) {
sharerClass, err := androidLoadClass(env, ctx, "org.julianfamily.keepassgo.AndroidShare")
if err != nil {
callErr = err
return
}
methodName := cString("shareVault")
defer C.free(unsafe.Pointer(methodName))
methodSig := cString("(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)V")
defer C.free(unsafe.Pointer(methodSig))
method := C.jni_GetStaticMethodID(env, sharerClass, methodName, methodSig)
if method == nil {
callErr = androidJNIError(env, "resolve shareVault method")
if callErr == nil {
callErr = fmt.Errorf("resolve shareVault method")
}
return
}
return fmt.Errorf("resolve shareVault method")
}
jPath := androidJavaString(env, path)
jTitle := androidJavaString(env, title)
args := [3]C.jvalue{}
args[0] = C.jni_ValueObject(ctx)
args[1] = C.jni_ValueObject(C.jobject(jPath))
args[2] = C.jni_ValueObject(C.jobject(jTitle))
C.jni_CallStaticVoidMethodA(env, sharerClass, method, &args[0])
if err := androidJNIError(env, "share vault"); err != nil {
return err
}
return nil
}
func androidJavaVM() (*C.JavaVM, error) {
var vms [1]*C.JavaVM
var count C.jsize
if res := C.jni_GetCreatedJavaVMs(&vms[0], 1, &count); res != C.JNI_OK {
return nil, fmt.Errorf("get created Java VMs: %d", int(res))
}
if count == 0 || vms[0] == nil {
return nil, fmt.Errorf("no Java VM available")
}
return vms[0], nil
jPath := androidJavaString(env, path)
jTitle := androidJavaString(env, title)
args := [3]C.jvalue{}
args[0] = C.jni_ValueObject(ctx)
args[1] = C.jni_ValueObject(C.jobject(jPath))
args[2] = C.jni_ValueObject(C.jobject(jTitle))
C.jni_CallStaticVoidMethodA(env, sharerClass, method, &args[0])
callErr = androidJNIError(env, "share vault")
})
return callErr
}
func androidLoadClass(env *C.JNIEnv, ctx C.jobject, name string) (C.jclass, error) {
+28 -4
View File
@@ -444,7 +444,12 @@ func TestUIAPITokenLifecycleManagement(t *testing.T) {
func TestUIAPITokenPolicyRulesCanBeAddedAndRemoved(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "vault.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"),
})
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
@@ -582,7 +587,12 @@ func TestPolicyRulePartsFormatsGroupAndEntryResources(t *testing.T) {
func TestUIAPITokenDetailPanelHandlesMissingRemoveClickables(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "vault.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"),
})
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
@@ -1615,10 +1625,17 @@ func TestUIOpenRemoteAndSaveThroughConfiguredWebDAVTarget(t *testing.T) {
}))
defer server.Close()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "vault.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"),
})
u.masterPassword.SetText("correct horse battery staple")
u.remoteBaseURL.SetText(server.URL)
u.remotePath.SetText("vaults/main.kdbx")
u.selectedVaultRemoteProfileID = ""
u.selectedVaultRemoteCredentialEntryID = ""
if err := u.openRemoteAction(); err != nil {
t.Fatalf("openRemoteAction() error = %v", err)
@@ -2059,10 +2076,17 @@ func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) {
}))
defer server.Close()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "vault.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"),
})
u.masterPassword.SetText("correct horse battery staple")
u.remoteBaseURL.SetText(server.URL)
u.remotePath.SetText("vaults/main.kdbx")
u.selectedVaultRemoteProfileID = ""
u.selectedVaultRemoteCredentialEntryID = ""
if err := u.openRemoteAction(); err != nil {
t.Fatalf("openRemoteAction() error = %v", err)