Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions explorer/dir_android.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package explorer

import (
"gioui.org/app"
"git.wow.st/gmp/jni"
"os"
)

//go:generate javac -source 8 -target 8 -bootclasspath $ANDROID_HOME/platforms/android-30/android.jar -d $TEMP/explorer_file_android/classes dir_android.java
//go:generate jar cf dir_android.jar -C $TEMP/explorer_file_android/classes .

type Directory struct {
createFile jni.MethodID
libClass jni.Class
libObject jni.Object

url jni.Object
openFile jni.MethodID
}

func newDirectory(env jni.Env, url jni.Object) (*Directory, error) {
f := &Directory{url: url}

class, err := jni.LoadClass(env, jni.ClassLoaderFor(env, jni.Object(app.AppContext())), "org/gioui/x/explorer/dir_android")
if err != nil {
return nil, err
}

obj, err := jni.NewObject(env, class, jni.GetMethodID(env, class, "<init>", `()V`))
if err != nil {
return nil, err
}

// For some reason, using `f.stream` as argument for a constructor (`public file_android(Object j) {}`) doesn't work.
if err := jni.CallVoidMethod(env, obj, jni.GetMethodID(env, class, "setHandle", `(Ljava/lang/Object;)V`), jni.Value(f.url)); err != nil {
return nil, err
}

f.libObject = jni.NewGlobalRef(env, obj)
f.libClass = jni.Class(jni.NewGlobalRef(env, jni.Object(class)))
Comment on lines +39 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where and how are these global references cleaned up? I don't see any mechanism to do that, and I don't think the JVM is able to do it automatically.

f.openFile = jni.GetMethodID(env, f.libClass, "readFile", "(Landroid/view/View;Ljava/lang/String;)Ljava/lang/Object;")
f.createFile = jni.GetMethodID(env, f.libClass, "writeFile", "(Landroid/view/View;Ljava/lang/String;)Ljava/lang/Object;")

return f, nil

}

func (d *Directory) ReadFile(name string) (file *File, err error) {
if d == nil || d.libObject == 0 {
return nil, os.ErrClosed
}

err = jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error {
nameJava := jni.JavaString(env, name)
stream, err := jni.CallObjectMethod(env, d.libObject, d.openFile, jni.Value(_View), jni.Value(nameJava))
if err != nil {
return err
}
if stream == 0 {
return os.ErrNotExist
}

file, err = newFile(env, stream)
return nil
})
if err != nil {
return nil, err
}

return file, nil
}

/*
func (d *Directory) OpenDirectory(name string) (Directory, error) {
// Implementation for opening a subdirectory in the directory
return nil, nil
}

*/

func (d *Directory) WriteFile(name string) (file *File, err error) {
if d == nil || d.libObject == 0 {
return nil, os.ErrClosed
}

err = jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error {
nameJava := jni.JavaString(env, name)
stream, err := jni.CallObjectMethod(env, d.libObject, d.createFile, jni.Value(_View), jni.Value(nameJava))
if err != nil {
return err
}
if stream == 0 {
return os.ErrExist
}

file, err = newFile(env, stream)
return nil
})
if err != nil {
return nil, err
}

return file, nil
}

/*
func (d *Directory) CreateDirectory(name string) (Directory, error) {
// Implementation for creating a subdirectory in the directory
return nil, nil
}

func (d *Directory) ListFiles() ([]File, error) {
// Implementation for listing files in the directory
return nil, nil
}

func (d *Directory) ListDirectories() ([]Directory, error) {
// Implementation for listing subdirectories in the directory
return nil, nil
}

*/
Binary file added explorer/dir_android.jar
Binary file not shown.
72 changes: 72 additions & 0 deletions explorer/dir_android.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.gioui.x.explorer;

import android.content.Context;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.database.Cursor;

import android.view.View;
import java.io.InputStream;
import java.io.OutputStream;

public class dir_android {
public Object handler;

public void setHandle(Object f) {
this.handler = f;
}

public Object readFile(View view, String filename) {
try {
Context ctx = view.getContext();
Uri folderUri = (Uri) handler;
Uri fileUri = findFile(ctx, folderUri, filename);
if (fileUri == null) {
return null;
}

return ctx.getContentResolver().openInputStream(fileUri);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public Object writeFile(View view, String filename) {
try {
Context ctx = view.getContext();
Uri folderUri = (Uri) handler;
Uri fileUri = findFile(ctx, folderUri, filename);

if (fileUri == null) {
fileUri = DocumentsContract.createDocument(ctx.getContentResolver(), folderUri, "application/octet-stream", filename);
}

return ctx.getContentResolver().openOutputStream(fileUri, "wt");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

private Uri findFile(Context ctx, Uri folderUri, String filename) {
String folderDocId = DocumentsContract.getDocumentId(folderUri);
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, folderDocId);

try (Cursor cursor = ctx.getContentResolver().query(childrenUri, new String[] {DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME},null, null, null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
String docId = cursor.getString(0);
String name = cursor.getString(1);
if (filename.equals(name)) {
return DocumentsContract.buildDocumentUriUsingTree(folderUri, docId);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}

return null;
}
}
13 changes: 13 additions & 0 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ func (e *Explorer) CreateFile(name string) (io.WriteCloser, error) {
return e.exportFile(name)
}

func (e *Explorer) OpenDirectory() (*Directory, error) {
if e == nil {
return nil, ErrNotAvailable
}

if runtime.GOOS != "js" {
e.mutex.Lock()
defer e.mutex.Unlock()
}

return e.openDirectory()
}

var (
DefaultExplorer *Explorer
)
Expand Down
47 changes: 41 additions & 6 deletions explorer/explorer_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ import (
//go:generate javac -source 8 -target 8 -bootclasspath $ANDROID_HOME/platforms/android-30/android.jar -d $TEMP/explorer_explorer_android/classes explorer_android.java
//go:generate jar cf explorer_android.jar -C $TEMP/explorer_explorer_android/classes .

var _View uintptr

type explorer struct {
window *app.Window
view uintptr

libObject jni.Object
libClass jni.Class

importFile jni.MethodID
exportFile jni.MethodID
importFile jni.MethodID
exportFile jni.MethodID
openDirectory jni.MethodID

result chan result
}
Expand Down Expand Up @@ -65,13 +68,15 @@ func (e *explorer) init(env jni.Env) error {
e.libClass = jni.Class(jni.NewGlobalRef(env, jni.Object(class)))
e.importFile = jni.GetMethodID(env, e.libClass, "importFile", "(Landroid/view/View;Ljava/lang/String;I)V")
e.exportFile = jni.GetMethodID(env, e.libClass, "exportFile", "(Landroid/view/View;Ljava/lang/String;I)V")
e.openDirectory = jni.GetMethodID(env, e.libClass, "openDirectory", "(Landroid/view/View;I)V")

return nil
}

func (e *Explorer) listenEvents(evt event.Event) {
if evt, ok := evt.(app.AndroidViewEvent); ok {
e.view = evt.View
_View = evt.View
}
}

Expand Down Expand Up @@ -136,17 +141,47 @@ func (e *Explorer) importFiles(_ ...string) ([]io.ReadCloser, error) {
return nil, ErrNotAvailable
}

func (e *Explorer) openDirectory() (*Directory, error) {
go e.window.Run(func() {
err := jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error {
if err := e.init(env); err != nil {
return err
}

return jni.CallVoidMethod(env, e.libObject, e.explorer.openDirectory,
jni.Value(e.view),
jni.Value(e.id),
)
})

if err != nil {
e.result <- result{error: err}
}
})

dir := <-e.result
if dir.error != nil {
return nil, dir.error
}
return dir.file.(*Directory), nil
}

//export Java_org_gioui_x_explorer_explorer_1android_ImportCallback
func Java_org_gioui_x_explorer_explorer_1android_ImportCallback(env *C.JNIEnv, _ C.jclass, stream C.jobject, id C.jint, err C.jstring) {
fileCallback(env, stream, id, err)
fileCallback(env, stream, id, err, newFile)
}

//export Java_org_gioui_x_explorer_explorer_1android_ExportCallback
func Java_org_gioui_x_explorer_explorer_1android_ExportCallback(env *C.JNIEnv, _ C.jclass, stream C.jobject, id C.jint, err C.jstring) {
fileCallback(env, stream, id, err)
fileCallback(env, stream, id, err, newFile)
}

//export Java_org_gioui_x_explorer_explorer_1android_DirectoryCallback
func Java_org_gioui_x_explorer_explorer_1android_DirectoryCallback(env *C.JNIEnv, _ C.jclass, url C.jobject, id C.jint, err C.jstring) {
fileCallback(env, url, id, err, newDirectory)
}

func fileCallback(env *C.JNIEnv, stream C.jobject, id C.jint, err C.jstring) {
func fileCallback[T any](env *C.JNIEnv, stream C.jobject, id C.jint, err C.jstring, fn func(env jni.Env, stream jni.Object) (T, error)) {
var res result
if v, ok := active.Load(int32(id)); ok {
env := jni.EnvFor(uintptr(unsafe.Pointer(env)))
Expand All @@ -158,7 +193,7 @@ func fileCallback(env *C.JNIEnv, stream C.jobject, id C.jint, err C.jstring) {
}
}
} else {
res.file, res.error = newFile(env, jni.NewGlobalRef(env, jni.Object(uintptr(stream))))
res.file, res.error = fn(env, jni.NewGlobalRef(env, jni.Object(uintptr(stream))))
}
v.(*explorer).result <- res
}
Expand Down
Binary file modified explorer/explorer_android.jar
Binary file not shown.
50 changes: 50 additions & 0 deletions explorer/explorer_android.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import android.os.Handler.Callback;
import android.os.Handler;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
Expand All @@ -30,10 +32,12 @@ public class explorer_android {
// List of requestCode used in the callback, to identify the caller.
static List<Integer> import_codes = new ArrayList<Integer>();
static List<Integer> export_codes = new ArrayList<Integer>();
static List<Integer> directory_codes = new ArrayList<Integer>();

// Functions defined on Golang.
static public native void ImportCallback(InputStream f, int id, String err);
static public native void ExportCallback(OutputStream f, int id, String err);
static public native void DirectoryCallback(Uri path, int id, String err);

public static class explorer_android_fragment extends Fragment {
Context context;
Expand Down Expand Up @@ -81,6 +85,29 @@ public void run() {
return;
}
}

if (directory_codes.contains(Integer.valueOf(requestCode))) {
directory_codes.remove(Integer.valueOf(requestCode));
if (resultCode != Activity.RESULT_OK) {
explorer_android.DirectoryCallback(null, requestCode, "");
activity.getFragmentManager().popBackStack();
return;
}
try {
Uri treeUri = data.getData();

int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
activity.getContentResolver().takePersistableUriPermission(treeUri, takeFlags);


Uri folderUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri));

explorer_android.DirectoryCallback(folderUri, requestCode, "");

} catch (Exception e) {
explorer_android.DirectoryCallback(null, requestCode, e.toString());
}
}
}
});

Expand Down Expand Up @@ -126,6 +153,29 @@ public void run() {
});
}

public void openDirectory(View view, int id) {
askPermission(view);

((Activity) view.getContext()).runOnUiThread(new Runnable() {
public void run() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Log.e("explorer_android", "openDirectory requires Android Lollipop or higher.");
explorer_android.DirectoryCallback(null, id, "Android version too low");
return;
}

registerFrag(view);
directory_codes.add(Integer.valueOf(id));

final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);

frag.startActivityForResult(Intent.createChooser(intent, ""), id);
}
});
}

public void registerFrag(View view) {
final Context ctx = view.getContext();
final FragmentManager fm;
Expand Down
Loading