From 6513195b9ed8e97af6f1aae93b2f1bd0ee140658 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Sat, 16 Mar 2024 18:14:46 +0100 Subject: [PATCH] implement file chooser using GtkFileChooserNative --- src/api-impl-jni/app/android_app_Activity.c | 56 +++++++++++++++++++ .../generated_headers/android_app_Activity.h | 8 +++ src/api-impl/android/app/Activity.java | 23 +++++++- .../android/content/ContentResolver.java | 8 +++ src/api-impl/android/content/Intent.java | 4 ++ src/api-impl/android/net/Uri.java | 6 +- .../android/os/ParcelFileDescriptor.java | 44 ++++++++++++--- 7 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/api-impl-jni/app/android_app_Activity.c b/src/api-impl-jni/app/android_app_Activity.c index 17c3d77c..185626c8 100644 --- a/src/api-impl-jni/app/android_app_Activity.c +++ b/src/api-impl-jni/app/android_app_Activity.c @@ -177,3 +177,59 @@ JNIEXPORT void JNICALL Java_android_app_Activity_nativeOpenURI(JNIEnv *env, jcla } extern GtkWindow *window; // TODO: get this in a better way + +struct filechooser_callback_data { jobject activity; jint request_code; }; + +#define RESULT_OK -1 +#define RESULT_CANCELED 0 +static void on_filechooser_response(GtkNativeDialog *native, int response, struct filechooser_callback_data *data) +{ + JNIEnv *env = get_jni_env(); + jmethodID fileChooserResultCallback = _METHOD(handle_cache.activity.class, "fileChooserResultCallback", "(IIILjava/lang/String;)V"); + + GtkFileChooser *chooser = GTK_FILE_CHOOSER(native); + GtkFileChooserAction action = gtk_file_chooser_get_action(chooser); + if (response == GTK_RESPONSE_ACCEPT) { + GFile *file = gtk_file_chooser_get_file(chooser); + char *uri = g_file_get_uri(file); + + (*env)->CallVoidMethod(env, data->activity, fileChooserResultCallback, data->request_code, RESULT_OK, action, _JSTRING(uri)); + if ((*env)->ExceptionCheck(env)) + (*env)->ExceptionDescribe(env); + + g_free(uri); + g_object_unref(file); + } else { + (*env)->CallVoidMethod(env, data->activity, fileChooserResultCallback, data->request_code, RESULT_CANCELED, action, NULL); + } + + g_object_unref(native); + _UNREF(data->activity); + free(data); +} + +JNIEXPORT void JNICALL Java_android_app_Activity_nativeFileChooser(JNIEnv *env, jobject this, jint action, jstring type_jstring, jstring filename_jstring, jint request_code) +{ + const char *chooser_title = ((char *[]){"Open File", "Save File", "Select Folder"})[action]; + GtkFileChooserNative *native = gtk_file_chooser_native_new(chooser_title, window, action, NULL, NULL); + + const char *type = type_jstring ? (*env)->GetStringUTFChars(env, type_jstring, NULL) : NULL; + if (type) { + GtkFileFilter *filter = gtk_file_filter_new(); + gtk_file_filter_add_mime_type(filter, type); + gtk_file_filter_set_name(filter, type); + gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(native), filter); + (*env)->ReleaseStringUTFChars(env, type_jstring, type); + } + const char *filename = filename_jstring ? (*env)->GetStringUTFChars(env, filename_jstring, NULL) : NULL; + if (filename) { + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(native), filename); + (*env)->ReleaseStringUTFChars(env, filename_jstring, filename); + } + + struct filechooser_callback_data *callback_data = malloc(sizeof(struct filechooser_callback_data)); + callback_data->activity = _REF(this); + callback_data->request_code = request_code; + g_signal_connect (native, "response", G_CALLBACK(on_filechooser_response), callback_data); + gtk_native_dialog_show (GTK_NATIVE_DIALOG (native)); +} diff --git a/src/api-impl-jni/generated_headers/android_app_Activity.h b/src/api-impl-jni/generated_headers/android_app_Activity.h index a53c45c5..12d1b91d 100644 --- a/src/api-impl-jni/generated_headers/android_app_Activity.h +++ b/src/api-impl-jni/generated_headers/android_app_Activity.h @@ -41,6 +41,14 @@ JNIEXPORT void JNICALL Java_android_app_Activity_nativeStartActivity JNIEXPORT void JNICALL Java_android_app_Activity_nativeOpenURI (JNIEnv *, jclass, jstring); +/* + * Class: android_app_Activity + * Method: nativeFileChooser + * Signature: (ILjava/lang/String;Ljava/lang/String;I)V + */ +JNIEXPORT void JNICALL Java_android_app_Activity_nativeFileChooser + (JNIEnv *, jobject, jint, jstring, jstring, jint); + #ifdef __cplusplus } #endif diff --git a/src/api-impl/android/app/Activity.java b/src/api-impl/android/app/Activity.java index 8e447132..ce6bf638 100644 --- a/src/api-impl/android/app/Activity.java +++ b/src/api-impl/android/app/Activity.java @@ -8,6 +8,7 @@ import android.content.ContextWrapper; import android.content.Intent; import android.content.res.Configuration; import android.content.res.XmlResourceParser; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -26,6 +27,7 @@ import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class Activity extends ContextWrapper implements Window.Callback { @@ -254,8 +256,20 @@ public class Activity extends ContextWrapper implements Window.Callback { protected void onActivityResult(int requestCode, int resultCode, Intent data) {} + // the order must match GtkFileChooserAction enum + private static final List FILE_CHOOSER_ACTIONS = Arrays.asList( + "android.intent.action.OPEN_DOCUMENT", // (0) GTK_FILE_CHOOSER_ACTION_OPEN + "android.intent.action.CREATE_DOCUMENT", // (1) GTK_FILE_CHOOSER_ACTION_SAVE + "android.intent.action.OPEN_DOCUMENT_TREE" // (2) GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER + ); + + // callback from native code + protected void fileChooserResultCallback(int requestCode, int resultCode, int action, String uri) { + onActivityResult(requestCode, resultCode, new Intent(FILE_CHOOSER_ACTIONS.get(action), uri != null ? Uri.parse(uri) : null)); + } + public void startActivityForResult(Intent intent, int requestCode, Bundle options) { - System.out.println("startActivityForResult(" + intent + ", " + requestCode + ") called, but we don't currently support multiple activities"); + System.out.println("startActivityForResult(" + intent + ", " + requestCode + ") called"); if (intent.getComponent() != null) { try { Class cls = Class.forName(intent.getComponent().getClassName()).asSubclass(Activity.class); @@ -272,12 +286,14 @@ public class Activity extends ContextWrapper implements Window.Callback { } }); } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - onActivityResult(requestCode, 0 /*RESULT_CANCELED*/, new Intent()); // RESULT_CANCELED is the only pre-defined return value, so hopefully it works out for us + onActivityResult(requestCode, 0 /*RESULT_CANCELED*/, new Intent()); } + } else if (FILE_CHOOSER_ACTIONS.contains(intent.getAction())) { + nativeFileChooser(FILE_CHOOSER_ACTIONS.indexOf(intent.getAction()), intent.getType(), intent.getStringExtra("android.intent.extra.TITLE"), requestCode); } else { System.out.println("startActivityForResult: intent was not handled. Calling onActivityResult(RESULT_CANCELED)."); - onActivityResult(requestCode, 0 /*RESULT_CANCELED*/, new Intent()); // RESULT_CANCELED is the only pre-defined return value, so hopefully it works out for us + onActivityResult(requestCode, 0 /*RESULT_CANCELED*/, new Intent()); } } public void startActivityForResult(Intent intent, int requestCode) { @@ -426,4 +442,5 @@ public class Activity extends ContextWrapper implements Window.Callback { public static native void nativeRecreateActivity(Activity activity); public static native void nativeStartActivity(Activity activity); public static native void nativeOpenURI(String uri); + public native void nativeFileChooser(int action, String type, String title, int requestCode); } diff --git a/src/api-impl/android/content/ContentResolver.java b/src/api-impl/android/content/ContentResolver.java index b46a8d42..bc089f4c 100644 --- a/src/api-impl/android/content/ContentResolver.java +++ b/src/api-impl/android/content/ContentResolver.java @@ -1,7 +1,11 @@ package android.content; +import java.io.File; +import java.io.FileNotFoundException; + import android.database.ContentObserver; import android.net.Uri; +import android.os.ParcelFileDescriptor; public class ContentResolver { public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer) { @@ -15,4 +19,8 @@ public class ContentResolver { } public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer, int userHandle) { } + + public ParcelFileDescriptor openFileDescriptor(Uri uri, String mode) throws FileNotFoundException { + return ParcelFileDescriptor.open(new File(uri.uri), ParcelFileDescriptor.parseMode(mode)); + } } diff --git a/src/api-impl/android/content/Intent.java b/src/api-impl/android/content/Intent.java index d2d55c81..5dc70608 100644 --- a/src/api-impl/android/content/Intent.java +++ b/src/api-impl/android/content/Intent.java @@ -290,4 +290,8 @@ public class Intent { public Parcelable[] getParcelableArrayExtra(String name) { return extras.getParcelableArray(name); } + + public String getType() { + return type; + } } diff --git a/src/api-impl/android/net/Uri.java b/src/api-impl/android/net/Uri.java index 01a52623..7ee5b57f 100644 --- a/src/api-impl/android/net/Uri.java +++ b/src/api-impl/android/net/Uri.java @@ -13,7 +13,7 @@ public class Uri implements Parcelable { public static final Uri EMPTY = new Uri(); - private URI uri; + public URI uri; public static Uri parse(String s) { Uri ret = new Uri(); @@ -160,6 +160,10 @@ public class Uri implements Parcelable { return uri.getPath(); } + public String getAuthority() { + return uri.getAuthority(); + } + @Override public String toString() { return String.valueOf(uri); diff --git a/src/api-impl/android/os/ParcelFileDescriptor.java b/src/api-impl/android/os/ParcelFileDescriptor.java index 6289e3d0..13b351ab 100644 --- a/src/api-impl/android/os/ParcelFileDescriptor.java +++ b/src/api-impl/android/os/ParcelFileDescriptor.java @@ -17,10 +17,21 @@ package android.os; import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.O_APPEND; +import static android.system.OsConstants.O_CLOEXEC; +import static android.system.OsConstants.O_CREAT; +import static android.system.OsConstants.O_RDONLY; +import static android.system.OsConstants.O_RDWR; +import static android.system.OsConstants.O_TRUNC; +import static android.system.OsConstants.O_WRONLY; import static android.system.OsConstants.SEEK_SET; import static android.system.OsConstants.SOCK_STREAM; +import static android.system.OsConstants.S_IROTH; +import static android.system.OsConstants.S_IRWXG; +import static android.system.OsConstants.S_IRWXU; import static android.system.OsConstants.S_ISLNK; import static android.system.OsConstants.S_ISREG; +import static android.system.OsConstants.S_IWOTH; import android.system.ErrnoException; import android.system.OsConstants; @@ -246,15 +257,30 @@ public class ParcelFileDescriptor implements Closeable { return pfd; } - private static FileDescriptor openInternal(File file, int mode) throws FileNotFoundException { /* - if ((mode & MODE_READ_WRITE) == 0) { - throw new IllegalArgumentException( - "Must specify MODE_READ_ONLY, MODE_WRITE_ONLY, or MODE_READ_WRITE"); - } - - final String path = file.getPath(); - return Parcel.openFileDescriptor(path, mode);*/ - return null; + private static FileDescriptor openInternal(File file, int mode) throws FileNotFoundException { + int flags = O_CLOEXEC; + if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) + flags |= O_RDWR; + else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) + flags |= O_WRONLY; + else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) + flags |= O_RDONLY; + else + throw new IllegalArgumentException("Bad mode: " + mode); + if ((mode & MODE_CREATE) == MODE_CREATE) + flags |= O_CREAT; + if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) + flags |= O_TRUNC; + if ((mode & MODE_APPEND) == MODE_APPEND) + flags |= O_APPEND; + int realMode = S_IRWXU | S_IRWXG; + if ((mode & MODE_WORLD_READABLE) != 0) realMode |= S_IROTH; + if ((mode & MODE_WORLD_WRITEABLE) != 0) realMode |= S_IWOTH; + try { + return android.system.Os.open(file.getPath(), flags, realMode); + } catch (ErrnoException e) { + throw new FileNotFoundException(e.getMessage()); + } } /**