From c4bc6dcb72f4a338c870de75f800d245c241e6c1 Mon Sep 17 00:00:00 2001 From: Alexandre Martins Date: Sun, 18 Feb 2024 05:03:22 -0300 Subject: [PATCH] Android: Native Dialog Addon (#1520) Android: add native dialog addon - Add `al_android_open_fd` - Adapt `ex_native_filechooser` --- addons/native_dialog/CMakeLists.txt | 6 + .../aintern_native_dialog_cfg.h.cmake | 1 + addons/native_dialog/android_dialog.c | 386 ++++++++++ .../org/liballeg/android/AllegroActivity.java | 62 +- .../org/liballeg/android/AllegroDialog.java | 659 ++++++++++++++++++ docs/src/refman/native_dialog.txt | 12 +- docs/src/refman/platform.txt | 45 ++ examples/CMakeLists.txt | 2 +- examples/ex_native_filechooser.c | 39 +- include/allegro5/allegro_android.h | 1 + src/android/android_system.c | 23 + 11 files changed, 1230 insertions(+), 6 deletions(-) create mode 100644 addons/native_dialog/android_dialog.c create mode 100644 android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroDialog.java diff --git a/addons/native_dialog/CMakeLists.txt b/addons/native_dialog/CMakeLists.txt index e34e14e376..5726ae70bd 100644 --- a/addons/native_dialog/CMakeLists.txt +++ b/addons/native_dialog/CMakeLists.txt @@ -30,6 +30,12 @@ if(APPLE AND IPHONE) set(SUPPORT_NATIVE_DIALOG 1) endif(APPLE AND IPHONE) +if(ANDROID) + list(APPEND NATIVE_DIALOG_SOURCES android_dialog.c) + set(ALLEGRO_CFG_NATIVE_DIALOG_ANDROID 1) + set(SUPPORT_NATIVE_DIALOG 1) +endif(ANDROID) + if(WIN32) list(APPEND NATIVE_DIALOG_SOURCES win_dialog.c) set(ALLEGRO_CFG_NATIVE_DIALOG_WINDOWS 1) diff --git a/addons/native_dialog/allegro5/internal/aintern_native_dialog_cfg.h.cmake b/addons/native_dialog/allegro5/internal/aintern_native_dialog_cfg.h.cmake index 3369e4979c..63bad0760c 100644 --- a/addons/native_dialog/allegro5/internal/aintern_native_dialog_cfg.h.cmake +++ b/addons/native_dialog/allegro5/internal/aintern_native_dialog_cfg.h.cmake @@ -1,3 +1,4 @@ #cmakedefine ALLEGRO_CFG_NATIVE_DIALOG_GTK #cmakedefine ALLEGRO_CFG_NATIVE_DIALOG_OSX #cmakedefine ALLEGRO_CFG_NATIVE_DIALOG_WINDOWS +#cmakedefine ALLEGRO_CFG_NATIVE_DIALOG_ANDROID diff --git a/addons/native_dialog/android_dialog.c b/addons/native_dialog/android_dialog.c new file mode 100644 index 0000000000..5bd7aec472 --- /dev/null +++ b/addons/native_dialog/android_dialog.c @@ -0,0 +1,386 @@ +/* ______ ___ ___ + * /\ _ \ /\_ \ /\_ \ + * \ \ \L\ \\//\ \ \//\ \ __ __ _ __ ___ + * \ \ __ \ \ \ \ \ \ \ /'__`\ /'_ `\/\`'__\/ __`\ + * \ \ \/\ \ \_\ \_ \_\ \_/\ __//\ \L\ \ \ \//\ \L\ \ + * \ \_\ \_\/\____\/\____\ \____\ \____ \ \_\\ \____/ + * \/_/\/_/\/____/\/____/\/____/\/___L\ \/_/ \/___/ + * /\____/ + * \_/__/ + * + * Native dialog addon for Android. + * + * By Alexandre Martins. + */ + +#include "allegro5/allegro.h" +#include "allegro5/allegro_native_dialog.h" +#include "allegro5/internal/aintern_android.h" +#include "allegro5/internal/aintern_native_dialog.h" + +ALLEGRO_DEBUG_CHANNEL("android") + +static ALLEGRO_DISPLAY *get_active_display(void); +static void wait_for_display_events(ALLEGRO_DISPLAY *dpy); +static bool open_file_chooser(int flags, const char *patterns, const char *initial_path, ALLEGRO_PATH ***out_uri_strings, size_t *out_uri_count); +static char *really_open_file_chooser(int flags, const char *patterns, const char *initial_path); +static int show_message_box(const char *title, const char *message, const char *buttons, int flags); +static void append_to_textlog(const char *tag, const char *message); + +static ALLEGRO_EVENT_QUEUE *queue = NULL; +static ALLEGRO_MUTEX *mutex = NULL; + + + + +bool _al_init_native_dialog_addon(void) +{ + if (NULL == (mutex = al_create_mutex())) + return false; + + if (NULL == (queue = al_create_event_queue())) { + al_destroy_mutex(mutex); + return false; + } + + return true; +} + +void _al_shutdown_native_dialog_addon(void) +{ + al_destroy_mutex(mutex); + mutex = NULL; + + /*al_destroy_event_queue(queue);*/ /* already released by Allegro's destructors */ + queue = NULL; +} + +bool _al_show_native_file_dialog(ALLEGRO_DISPLAY *display, ALLEGRO_NATIVE_DIALOG *fd) +{ + /* al_show_native_file_dialog() has a blocking interface. Since there is a + need to handle drawing halt and drawing resume events before this + function returns, users must call it from a separate thread. */ + (void)display; /* possibly NULL */ + + /* fail if the native dialog addon is not initialized */ + if (!al_is_native_dialog_addon_initialized()) { + ALLEGRO_DEBUG("the native dialog addon is not initialized"); + return false; + } + + /* only one file chooser may be opened at a time */ + al_lock_mutex(mutex); + + /* initial path (optional) */ + const char *initial_path = NULL; + if (fd->fc_initial_path != NULL) + initial_path = al_path_cstr(fd->fc_initial_path, '/'); + + /* release any pre-existing paths */ + if (fd->fc_paths != NULL) { + for (size_t i = 0; i < fd->fc_path_count; i++) + al_destroy_path(fd->fc_paths[i]); + al_free(fd->fc_paths); + } + + /* register the event source */ + ALLEGRO_DISPLAY *dpy = get_active_display(); + if (dpy != NULL) + al_register_event_source(queue, &dpy->es); + + /* open the file chooser */ + ALLEGRO_DEBUG("waiting for the file chooser"); + bool ret = open_file_chooser(fd->flags, al_cstr(fd->fc_patterns), initial_path, &fd->fc_paths, &fd->fc_path_count); + ALLEGRO_DEBUG("done waiting for the file chooser"); + + /* ensure predictable behavior */ + if (dpy != NULL) { + + /* We expect al_show_native_file_dialog() to be called before a drawing + halt. We expect it to return after a drawing resume. */ + wait_for_display_events(dpy); + + /* unregister the event source */ + al_unregister_event_source(queue, &dpy->es); + + } + + /* done! */ + ALLEGRO_DEBUG("done"); + + al_unlock_mutex(mutex); + return ret; +} + +int _al_show_native_message_box(ALLEGRO_DISPLAY *display, ALLEGRO_NATIVE_DIALOG *nd) +{ + const char *heading = al_cstr(nd->mb_heading); + const char *text = al_cstr(nd->mb_text); + const char *buttons = nd->mb_buttons != NULL ? al_cstr(nd->mb_buttons) : NULL; + (void)display; + + return show_message_box(heading, text, buttons, nd->flags); +} + +bool _al_open_native_text_log(ALLEGRO_NATIVE_DIALOG *textlog) +{ + textlog->is_active = true; + return true; +} + +void _al_close_native_text_log(ALLEGRO_NATIVE_DIALOG *textlog) +{ + textlog->is_active = false; +} + +void _al_append_native_text_log(ALLEGRO_NATIVE_DIALOG *textlog) +{ + if (textlog->is_active) { + const char *title = al_cstr(textlog->title); + const char *text = al_cstr(textlog->tl_pending_text); + + append_to_textlog(title, text); + + al_ustr_truncate(textlog->tl_pending_text, 0); + } +} + +bool _al_init_menu(ALLEGRO_MENU *menu) +{ + (void)menu; + return false; +} + +bool _al_init_popup_menu(ALLEGRO_MENU *menu) +{ + (void)menu; + return false; +} + +bool _al_insert_menu_item_at(ALLEGRO_MENU_ITEM *item, int i) +{ + (void)item; + (void)i; + return false; +} + +bool _al_destroy_menu_item_at(ALLEGRO_MENU_ITEM *item, int i) +{ + (void)item; + (void)i; + return false; +} + +bool _al_update_menu_item_at(ALLEGRO_MENU_ITEM *item, int i) +{ + (void)item; + (void)i; + return false; +} + +bool _al_show_display_menu(ALLEGRO_DISPLAY *display, ALLEGRO_MENU *menu) +{ + (void)display; + (void)menu; + return false; +} + +bool _al_hide_display_menu(ALLEGRO_DISPLAY *display, ALLEGRO_MENU *menu) +{ + (void)display; + (void)menu; + return false; +} + +bool _al_show_popup_menu(ALLEGRO_DISPLAY *display, ALLEGRO_MENU *menu) +{ + (void)display; + (void)menu; + return false; +} + +int _al_get_menu_display_height(void) +{ + return 0; +} + + + + +ALLEGRO_DISPLAY *get_active_display(void) +{ + ALLEGRO_SYSTEM *sys = al_get_system_driver(); + ASSERT(sys); + + if (_al_vector_size(&sys->displays) == 0) + return NULL; + + ALLEGRO_DISPLAY **dptr = (ALLEGRO_DISPLAY **)_al_vector_ref(&sys->displays, 0); + return *dptr; +} + +void wait_for_display_events(ALLEGRO_DISPLAY *dpy) +{ + ALLEGRO_DISPLAY_ANDROID *d = (ALLEGRO_DISPLAY_ANDROID *)dpy; + ALLEGRO_TIMEOUT timeout; + ALLEGRO_EVENT event; + bool expected_state = false; + + memset(&event, 0, sizeof(event)); + + /* We expect a drawing halt event to be on the queue. If that is not true, + then the drawing halt did not take place for some unusual reason */ + ALLEGRO_DEBUG("looking for a ALLEGRO_EVENT_DISPLAY_HALT_DRAWING"); + while (al_get_next_event(queue, &event)) { + if (event.type == ALLEGRO_EVENT_DISPLAY_HALT_DRAWING) { + expected_state = true; + break; + } + } + + /* skip if we're in an unexpected state */ + if (!expected_state) { + ALLEGRO_DEBUG("ALLEGRO_EVENT_DISPLAY_HALT_DRAWING not found"); + return; + } + + wait_for_drawing_resume: + + /* wait for ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING */ + ALLEGRO_DEBUG("waiting for ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING"); + while (event.type != ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING) + al_wait_for_event(queue, &event); + ALLEGRO_DEBUG("done waiting for ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING"); + + /* wait for al_acknowledge_drawing_resume() */ + ALLEGRO_DEBUG("waiting for al_acknowledge_drawing_resume"); + al_lock_mutex(d->mutex); + while (!d->resumed) + al_wait_cond(d->cond, d->mutex); + al_unlock_mutex(d->mutex); + ALLEGRO_DEBUG("done waiting for al_acknowledge_drawing_resume"); + + /* A resize event takes place here, as can be seen in the implementation of + AllegroSurface.nativeOnChange() at src/android_display.c (at the time of + this writing). However, the Allegro documentation does not specify that + a resize event must follow a drawing resume. + + We expect that the user will call al_acknowledge_resize() immediately. + We don't wait for the acknowledgement of the resize event, in case the + implementation changes someday. We just wait a little bit. */ + ; + + /* check if a new ALLEGRO_EVENT_DISPLAY_HALT_DRAWING is emitted */ + ALLEGRO_DEBUG("waiting for another ALLEGRO_EVENT_DISPLAY_HALT_DRAWING"); + al_init_timeout(&timeout, 0.5); + while (al_wait_for_event_until(queue, &event, &timeout)) { + if (event.type == ALLEGRO_EVENT_DISPLAY_HALT_DRAWING) + goto wait_for_drawing_resume; + else if (event.type == ALLEGRO_EVENT_DISPLAY_RESIZE) + al_init_timeout(&timeout, 0.5); + } + ALLEGRO_DEBUG("done waiting for another ALLEGRO_EVENT_DISPLAY_HALT_DRAWING"); +} + +bool open_file_chooser(int flags, const char *patterns, const char *initial_path, ALLEGRO_PATH ***out_uri_strings, size_t *out_uri_count) +{ + const char URI_DELIMITER = '\n'; + char *result = NULL; + + /* initialize the results */ + *out_uri_count = 0; + *out_uri_strings = NULL; + + /* open the file chooser */ + result = really_open_file_chooser(flags, patterns, initial_path); + + /* error? */ + if (result == NULL) + return false; + + /* split the returned string. If the file chooser was cancelled, variable result is an empty string */ + for (char *next_uri = result, *p = result; *p; p++) { + if (*p == URI_DELIMITER) { + int last = (*out_uri_count)++; + *out_uri_strings = al_realloc(*out_uri_strings, (*out_uri_count) * sizeof(ALLEGRO_PATH**)); + + /* ALLEGRO_PATHs don't explicitly support URIs at this time, but this works nonetheless. + See parse_path_string() at src/path.c */ + *p = '\0'; + (*out_uri_strings)[last] = al_create_path(next_uri); + next_uri = p+1; + } + } + + /* success! */ + free(result); + return true; +} + +char *really_open_file_chooser(int flags, const char *patterns, const char *initial_path) +{ + char *result = NULL; + + JNIEnv *env = _al_android_get_jnienv(); + jobject activity = _al_android_activity_object(); + jobject dialog = _jni_callObjectMethod(env, activity, "getNativeDialogAddon", "()L" ALLEGRO_ANDROID_PACKAGE_NAME_SLASH "/AllegroDialog;"); + + jstring jpatterns = _jni_call(env, jstring, NewStringUTF, patterns != NULL ? patterns : ""); + jstring jinitial_path = _jni_call(env, jstring, NewStringUTF, initial_path != NULL ? initial_path : ""); + + jstring jresult = (jstring)_jni_callObjectMethodV(env, dialog, "openFileChooser", "(ILjava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (jint)flags, jpatterns, jinitial_path); + jboolean is_null_result = _jni_call(env, jboolean, IsSameObject, jresult, NULL); + if (!is_null_result) { + const char *tmp = _jni_call(env, const char*, GetStringUTFChars, jresult, NULL); + result = strdup(tmp); + _jni_callv(env, ReleaseStringUTFChars, jresult, tmp); + } + _jni_callv(env, DeleteLocalRef, jresult); + + _jni_callv(env, DeleteLocalRef, jinitial_path); + _jni_callv(env, DeleteLocalRef, jpatterns); + + _jni_callv(env, DeleteLocalRef, dialog); + + return result; +} + +int show_message_box(const char *title, const char *message, const char *buttons, int flags) +{ + JNIEnv *env = _al_android_get_jnienv(); + jobject activity = _al_android_activity_object(); + jobject dialog = _jni_callObjectMethod(env, activity, "getNativeDialogAddon", "()L" ALLEGRO_ANDROID_PACKAGE_NAME_SLASH "/AllegroDialog;"); + + jstring jtitle = _jni_call(env, jstring, NewStringUTF, title != NULL ? title : ""); + jstring jmessage = _jni_call(env, jstring, NewStringUTF, message != NULL ? message : ""); + jstring jbuttons = _jni_call(env, jstring, NewStringUTF, buttons != NULL ? buttons : ""); + + int result = _jni_callIntMethodV(env, dialog, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)I", jtitle, jmessage, jbuttons, (jint)flags); + + _jni_callv(env, DeleteLocalRef, jbuttons); + _jni_callv(env, DeleteLocalRef, jmessage); + _jni_callv(env, DeleteLocalRef, jtitle); + + _jni_callv(env, DeleteLocalRef, dialog); + + return result; +} + +void append_to_textlog(const char *tag, const char *message) +{ + JNIEnv *env = _al_android_get_jnienv(); + jobject activity = _al_android_activity_object(); + jobject dialog = _jni_callObjectMethod(env, activity, "getNativeDialogAddon", "()L" ALLEGRO_ANDROID_PACKAGE_NAME_SLASH "/AllegroDialog;"); + + jstring jtag = _jni_call(env, jstring, NewStringUTF, tag != NULL ? tag : ""); + jstring jmessage = _jni_call(env, jstring, NewStringUTF, message != NULL ? message : ""); + + _jni_callVoidMethodV(env, dialog, "appendToTextLog", "(Ljava/lang/String;Ljava/lang/String;)V", jtag, jmessage); + + _jni_callv(env, DeleteLocalRef, jmessage); + _jni_callv(env, DeleteLocalRef, jtag); + + _jni_callv(env, DeleteLocalRef, dialog); +} + +/* vim: set sts=4 sw=4 et: */ diff --git a/android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroActivity.java b/android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroActivity.java index df93173060..9b24286a14 100644 --- a/android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroActivity.java +++ b/android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroActivity.java @@ -1,6 +1,7 @@ package org.liballeg.android; import android.app.Activity; +import android.content.Intent; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -28,6 +29,9 @@ import android.view.KeyEvent; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import java.io.FileNotFoundException; public class AllegroActivity extends Activity { @@ -43,6 +47,7 @@ public class AllegroActivity extends Activity private Vector joysticks; private Clipboard clipboard; private DisplayManager.DisplayListener displayListener; + private AllegroDialog dialog = null; public final static int JS_A = 0; public final static int JS_B = 1; @@ -316,6 +321,7 @@ public void run() { } public void updateOrientation() { + dismissMessageBox(); nativeOnOrientationChange(getAllegroOrientation(), false); } @@ -372,7 +378,7 @@ public void onCreate(Bundle savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - if(Build.VERSION.SDK_INT >= 33) { + if (Build.VERSION.SDK_INT >= 33) { // handle the back button / gesture on API level 33+ getOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, new OnBackInvokedCallback() { @@ -446,6 +452,7 @@ public void onPause() Log.d("AllegroActivity", "onPause"); sensors.unlisten(); + dismissMessageBox(); nativeOnPause(); Log.d("AllegroActivity", "onPause end"); @@ -569,6 +576,29 @@ String getOsVersion() return android.os.Build.VERSION.RELEASE; } + public int openFileDescriptor(String uriString, String mode) + { + final int NOT_FOUND = -1, UNSUPPORTED = -2; + + if (Build.VERSION.SDK_INT < 12) + return UNSUPPORTED; + + try { + // See https://developer.android.com/reference/android/content/ContentResolver#openFileDescriptor(android.net.Uri,%20java.lang.String) + Uri uri = Uri.parse(uriString); // content:// or file:// + ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, mode); + + // the user is responsible for closing this file descriptor + return pfd.detachFd(); // API 12+ + } + catch (FileNotFoundException e) { + // permission issues? + Log.e("AllegroActivity", "openFileDescriptor: file not found or invalid mode"); + } + + return NOT_FOUND; + } + private boolean isJoystick(int id) { InputDevice input = InputDevice.getDevice(id); int sources = input.getSources(); @@ -686,6 +716,36 @@ public void setAllegroFrameless(final boolean on) { }); } } + + public AllegroDialog getNativeDialogAddon() + { + if (dialog == null) + dialog = new AllegroDialog(this); // lazy instantiation + + return dialog; + } + + private void dismissMessageBox() + { + // Message boxes block the calling thread. If the app receives a drawing + // halt/resume or a display resize event, Allegro will block until these + // events are acknowledged. If they are emitted while a message box is + // visible and if their acknowledgement takes place in the same thread + // that spawned the message box, then the app will freeze. A deadlock + // will occur because the events can't be acknowledged while the message + // box is blocking the same thread. We should not block in this case. + if (dialog != null) + dialog.dismissMessageBox(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent resultData) + { + super.onActivityResult(requestCode, resultCode, resultData); + + if (dialog != null) + dialog.onActivityResult(requestCode, resultCode, resultData); + } } /* vim: set sts=3 sw=3 et: */ diff --git a/android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroDialog.java b/android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroDialog.java new file mode 100644 index 0000000000..084ac10fe4 --- /dev/null +++ b/android/gradle_project/allegro/src/main/java/org/liballeg/android/AllegroDialog.java @@ -0,0 +1,659 @@ +package org.liballeg.android; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + + +/** + * Native Dialog Addon + */ +public class AllegroDialog +{ + private final AllegroActivity activity; + private final AllegroMessageBox messageBox = new AllegroMessageBox(); + private final AllegroFileChooser fileChooser = new AllegroFileChooser(); + private final AllegroTextLog textLog = new AllegroTextLog(); + + public AllegroDialog(AllegroActivity activity) + { + this.activity = activity; + } + + public int showMessageBox(String title, String message, String buttons, int flags) + { + return messageBox.show(activity, title, message, buttons, flags); + } + + public void dismissMessageBox() + { + messageBox.dismiss(); + } + + public void appendToTextLog(String tag, String message) + { + textLog.append(tag, message); + } + + public String openFileChooser(int flags, String patterns, String initialPath) + { + return fileChooser.open(activity, flags, patterns, initialPath); + } + + public void onActivityResult(int requestCode, int resultCode, Intent resultData) + { + if (requestCode == AllegroFileChooser.REQUEST_FILE_CHOOSER) + fileChooser.onActivityResult(requestCode, resultCode, resultData); + } +} + + +/** + * A Message Box implementation + */ +class AllegroMessageBox +{ + private static final String TAG = "AllegroMessageBox"; + private static final int NO_BUTTON = 0; + private static final int POSITIVE_BUTTON = 1; + private static final int NEGATIVE_BUTTON = 2; + private static final int NEUTRAL_BUTTON = 3; + private AlertDialog currentDialog = null; + + public int show(final Activity activity, final String title, final String message, final String buttons, final int flags) + { + final MutableInteger result = new MutableInteger(NO_BUTTON); // use a boxed integer + final Looper looper = myLooper(); + + // create and show an alert dialog + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + // create an alert dialog + AlertDialog newDialog = createAlertDialog(activity, title, message, buttons, flags, result, looper); + + // only one alert dialog can be active at any given time + if (currentDialog != null) + currentDialog.dismiss(); + + // show the alert dialog + currentDialog = newDialog; + showAlertDialog(currentDialog, activity); + } + }); + + // make the dialog a modal + Log.d(TAG, "Waiting for user input"); + try { Looper.loop(); } catch (LooperInterrupterException e) { ; } + Log.d(TAG, "Result = " + result.get()); + + // done! + return result.get(); + } + + public void dismiss() + { + if (currentDialog != null) { + Log.d(TAG, "Dismissed by Allegro"); + currentDialog.dismiss(); + } + } + + private AlertDialog createAlertDialog(Activity activity, String title, String message, String buttons, int flags, MutableInteger result, Looper looper) + { + final LooperInterrupter interrupter = new LooperInterrupter(looper); + OnClickListenerGenerator resultSetter = new OnClickListenerGenerator(result); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + // configure the alert dialog + builder.setTitle(title); + builder.setMessage(message); + builder.setCancelable(true); + + if (0 != (flags & AllegroDialogConst.ALLEGRO_MESSAGEBOX_WARN)) + builder.setIcon(android.R.drawable.ic_dialog_alert); + else if (0 != (flags & AllegroDialogConst.ALLEGRO_MESSAGEBOX_ERROR)) + builder.setIcon(android.R.drawable.ic_dialog_alert); // ic_delete + else if (0 != (flags & AllegroDialogConst.ALLEGRO_MESSAGEBOX_QUESTION)) + builder.setIcon(android.R.drawable.ic_dialog_info); + + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + dialog.dismiss(); + } + }); + + builder.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (dialog == currentDialog) + currentDialog = null; + + interrupter.interrupt(); + } + }); + + // configure the buttons + boolean wantCustomButtons = !buttons.equals(""); + String[] buttonText = wantCustomButtons ? buttons.split("\\|") : null; + + if (!wantCustomButtons) { + builder.setPositiveButton(android.R.string.ok, resultSetter.generate(POSITIVE_BUTTON)); + + if (0 != (flags & (AllegroDialogConst.ALLEGRO_MESSAGEBOX_OK_CANCEL | AllegroDialogConst.ALLEGRO_MESSAGEBOX_YES_NO))) { + // unfortunately, android.R.string.yes and android.R.string.no + // are deprecated and resolve to android.R.string.ok and + // android.R.string.cancel, respectively. + builder.setNegativeButton(android.R.string.cancel, resultSetter.generate(NEGATIVE_BUTTON)); + } + } + else if (buttonText.length == 1) { + builder.setPositiveButton(buttonText[0], resultSetter.generate(POSITIVE_BUTTON)); + } + else if (buttonText.length == 2) { + builder.setPositiveButton(buttonText[0], resultSetter.generate(POSITIVE_BUTTON)); + builder.setNegativeButton(buttonText[1], resultSetter.generate(NEGATIVE_BUTTON)); + } + else if (buttonText.length >= 3) { // we support up to 3 buttons + builder.setPositiveButton(buttonText[0], resultSetter.generate(POSITIVE_BUTTON)); + builder.setNegativeButton(buttonText[1], resultSetter.generate(NEGATIVE_BUTTON)); + builder.setNeutralButton(buttonText[2], resultSetter.generate(NEUTRAL_BUTTON)); + } + + // create the alert dialog + return builder.create(); + } + + private void showAlertDialog(AlertDialog dialog, Activity activity) + { + ImmersiveDialogWrapper dlg = new ImmersiveDialogWrapper(dialog); + dlg.show(activity); + } + + private Looper myLooper() + { + // Looper.prepare() will raise an exception if a Looper already exists in the thread + try { Looper.prepare(); } catch (Exception e) { ; } + return Looper.myLooper(); + } + + private class LooperInterrupter extends Handler + { + public LooperInterrupter(Looper looper) + { + super(looper); + } + + public void interrupt() + { + sendMessage(obtainMessage()); + } + + @Override + public void handleMessage(Message msg) + { + throw new LooperInterrupterException(); + } + } + + private class LooperInterrupterException extends RuntimeException + { + } + + private class MutableInteger + { + private int value; + + public MutableInteger(int value) + { + this.value = value; + } + + public void set(int value) + { + this.value = value; + } + + public int get() + { + return value; + } + } + + private class OnClickListenerGenerator + { + private final MutableInteger result; + + public OnClickListenerGenerator(MutableInteger result) + { + this.result = result; + } + + public DialogInterface.OnClickListener generate(final int value) + { + return new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.set(value); + dialog.dismiss(); + } + }; + } + } + + private class ImmersiveDialogWrapper + { + /* + + This class wraps a Dialog object with code that handles the immersive + mode in a special way. + + Message boxes have a blocking interface. When the app is in immersive + mode (i.e., the ALLEGRO_FRAMELESS display flag is on), a deadlock may + occur as soon as a message box is invoked. That will happen if the + thread that calls al_acknowledge_resize() is the same thread that + invokes the message box. + + Without a special handling of the immersive mode, the Android system + will momentarily show the navigation bar and trigger a surface change + as soon as the message box is called forth. This, in turn, triggers a + display resize event. In summary, here is what happens: + + - Allegro will block the UI thread until ALLEGRO_EVENT_DISPLAY_RESIZE + is acknowledged. + + - The thread that invoked the message box from native code will remain + blocked until the alert dialog is dismissed. + + - The thread that invoked the message box cannot possibly acknowledge + the display resize while it is blocked. + + - The alert dialog is displayed from the UI thread. If the UI thread + is blocked, then the dialog can't be dismissed. If the dialog isn't + dismissed, then the thread that invoked the message box will remain + blocked. + + - Unless al_acknowledge_resize() is called by some other thread, then + both threads will be waiting forever. Deadlock! + + The solution below prevents the deadlock from occurring by preventing + the triggering of a display resize event in the first place. This is + achieved with a little trick that makes the dialog non-focusable while + it's being created. + + */ + private final Dialog dialog; + + public ImmersiveDialogWrapper(Dialog dialog) + { + this.dialog = dialog; + } + + public void show(Activity activity) + { + if (!isInImmersiveMode(activity)) { + dialog.show(); + return; + } + + // This solution is based on https://stackoverflow.com/a/23207365 + View view = activity.getWindow().getDecorView(); + int immersiveFlags = view.getSystemUiVisibility(); + Window dialogWindow = dialog.getWindow(); + + dialogWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + + dialog.show(); + + dialogWindow.getDecorView().setSystemUiVisibility(immersiveFlags); + dialogWindow.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + } + + private boolean isInImmersiveMode(Activity activity) + { + if (Build.VERSION.SDK_INT < 19) + return false; + + // This is based on AllegroActivity.setAllegroFrameless() + View view = activity.getWindow().getDecorView(); + int flags = view.getSystemUiVisibility(); + final int IMMERSIVE_FLAGS = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + + return (flags & IMMERSIVE_FLAGS) != 0; + } + } +} + + +/** + * A file chooser based on the Storage Access Framework (API level 19+) + */ +class AllegroFileChooser +{ + public static final int REQUEST_FILE_CHOOSER = 0xF11E; + + private static final String TAG = "AllegroFileChooser"; + private static final String URI_DELIMITER = "\n"; // the Line Feed character cannot be part of a URI (RFC 3986) + private CountDownLatch signal = null; + private Uri[] resultUri = null; + private boolean canceled = false; + + public String open(Activity activity, int flags, String patterns, String initialPath) + { + String result; + + try { + // only one file chooser can be opened at any given time + if (signal != null) + throw new UnsupportedOperationException("Only one file chooser can be opened at any given time"); + signal = new CountDownLatch(1); + + // reset the result + resultUri = null; + canceled = false; + + // get an URI from the initial path + Uri initialUri = getUriFromInitialPath(initialPath); + + // get the mime types from the patterns + String[] mimeTypes = getMimeTypesFromPatterns(patterns); + Log.d(TAG, "Patterns: " + patterns); + Log.d(TAG, "Mime types: " + TextUtils.join(";", mimeTypes)); + + // open the file chooser + // note: onActivityResult() will be called on the UI thread + reallyOpen(activity, flags, mimeTypes, initialUri); + + /* + + The Allegro API specifies that al_show_native_file_dialog() blocks + the calling thread until it returns. Since there is a need to + handle ALLEGRO_EVENT_DISPLAY_HALT_DRAWING before this function + returns, this must be called from a different thread. + + If al_show_native_file_dialog() is not called from a different + thread, then the app will freeze. That will happen after the + Activity is paused and before it is stopped. The freezing will + take place in two places: at AllegroSurface.nativeOnDestroy() and + in here. + + 1) Here we wait for onActivityResult(), which is called after the + we have the results of the file chooser. + + 2) At AllegroSurface.nativeOnDestroy(), we find a condition variable + that waits for al_acknowledge_drawing_halt(). + + Such acknowledgement cannot happen because the file chooser is + blocking. This fact, in turn, freezes the normal life cycle of the + Activity after onPause() and before onStopped(). As a consequence, + since onActivityResult() would be called by the system only after + onStopped() and before onRestart(), we never get a result from the + file chooser and the app freezes. + + Solution: call al_show_native_file_dialog() from another thread. + + */ + + // block the calling thread + Log.d(TAG, "Waiting for user input"); + signal.await(); + signal = null; + Log.d(TAG, "The file chooser has just been closed!"); + } + catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + resultUri = null; + } + + // process the result + // due to the constraints of Scoped Storage, we return content:// URIs instead of file paths + if (resultUri != null) { + Log.d(TAG, "Result: success"); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < resultUri.length; i++) { + sb.append(resultUri[i].toString()); + sb.append(URI_DELIMITER); + } + result = sb.toString(); + } + else if (canceled) { + Log.d(TAG, "Result: canceled"); + result = ""; + } + else { + Log.d(TAG, "Result: failure"); + result = null; + } + + // done! + resultUri = null; + return result; + } + + public void onActivityResult(int requestCode, int resultCode, Intent resultData) + { + if (requestCode != REQUEST_FILE_CHOOSER) + return; + + // canceled? + if (resultCode != Activity.RESULT_OK || resultData == null) { + canceled = (resultCode != Activity.RESULT_OK); + close(null); + return; + } + + // check for multiple results + if (Build.VERSION.SDK_INT >= 16) { + android.content.ClipData clipData = resultData.getClipData(); + if (clipData != null) { + // multiple results + int n = clipData.getItemCount(); + Uri[] uri = new Uri[n]; + for (int i = 0; i < n; i++) + uri[i] = clipData.getItemAt(i).getUri(); + close(uri); + return; + } + } + + // single result + Uri uri = resultData.getData(); + if (uri != null) { + close(new Uri[] { uri }); + return; + } + + // shouldn't happen + close(null); + } + + private void close(Uri[] result) + { + // store the result, which is null or an array of content:// URIs + resultUri = result; + + // unblock the calling thread + if (signal != null) + signal.countDown(); + } + + private void reallyOpen(Activity activity, int flags, String[] mimeTypes, Uri initialUri) throws RuntimeException + { + if (Build.VERSION.SDK_INT < 19) + throw new UnsupportedOperationException("Unsupported operation in API level " + Build.VERSION.SDK_INT); + + String action = selectAction(flags); + Intent intent = new Intent(action); + + if (0 != (flags & AllegroDialogConst.ALLEGRO_FILECHOOSER_SAVE)) { + // "Save file": must indicate a mime type + intent.setType("application/octet-stream"); // binary + //intent.setType("text/plain"); // plain text + if (mimeTypes.length > 0 && !mimeTypes[0].equals("*/*")) + intent.setType(mimeTypes[0]); + } + else if (0 == (flags & AllegroDialogConst.ALLEGRO_FILECHOOSER_FOLDER)) { + // "Load file" + if (mimeTypes.length != 1) { + intent.setType("*/*"); + if (mimeTypes.length > 0) + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); + else if (0 != (flags & AllegroDialogConst.ALLEGRO_FILECHOOSER_PICTURES)) + intent.setType("image/*"); + } + else + intent.setType(mimeTypes[0]); + } + + if (0 == (flags & AllegroDialogConst.ALLEGRO_FILECHOOSER_FOLDER)) + intent.addCategory(Intent.CATEGORY_OPENABLE); + + if (0 != (flags & AllegroDialogConst.ALLEGRO_FILECHOOSER_MULTIPLE)) + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + if (Build.VERSION.SDK_INT >= 26) { + if (initialUri != null) + intent.putExtra(android.provider.DocumentsContract.EXTRA_INITIAL_URI, initialUri); + } + + // invoke the intent + Log.d(TAG, "invoking intent " + action); + activity.startActivityForResult(intent, REQUEST_FILE_CHOOSER); // throws ActivityNotFoundException + + /* + + "To support media file access on devices that run Android 9 (API level + 28) or lower, declare the READ_EXTERNAL_STORAGE permission and set the + maxSdkVersion to 28." + + https://developer.android.com/training/data-storage/shared/documents-files + + */ + } + + private String selectAction(int flags) throws UnsupportedOperationException + { + if (0 != (flags & AllegroDialogConst.ALLEGRO_FILECHOOSER_FOLDER)) { + if (Build.VERSION.SDK_INT < 21) + throw new UnsupportedOperationException("Unsupported operation in API level " + Build.VERSION.SDK_INT); + + return Intent.ACTION_OPEN_DOCUMENT_TREE; + } + + if (0 != (flags & AllegroDialogConst.ALLEGRO_FILECHOOSER_SAVE)) { + /* + + "ACTION_CREATE_DOCUMENT cannot overwrite an existing file. If your + app tries to save a file with the same name, the system appends a + number in parentheses at the end of the file name. + + For example, if your app tries to save a file called confirmation.pdf + in a directory that already has a file with that name, the system + saves the new file with the name confirmation(1).pdf." + + https://developer.android.com/training/data-storage/shared/documents-files#create-file + + */ + + return Intent.ACTION_CREATE_DOCUMENT; + } + + return Intent.ACTION_OPEN_DOCUMENT; + } + + String[] getMimeTypesFromPatterns(String patterns) + { + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + String[] pattern = patterns.toLowerCase().split(";"); + ArrayList mimeType = new ArrayList(); + + for (int i = 0; i < pattern.length; i++) { + String mime = getMimeTypeFromPattern(pattern[i], mimeTypeMap); + if (mime != null) // we discard unknown mime types + mimeType.add(mime); + } + + return mimeType.toArray(new String[0]); + } + + private String getMimeTypeFromPattern(String pattern, MimeTypeMap mimeTypeMap) + { + // a pattern may be a mime type or an extension + if (pattern.contains("/")) + return pattern; // assume it's a mime type + else if (pattern.equals("") || pattern.equals(".") || pattern.equals("*.")) + return null; // ignore empty + else if (pattern.equals("*.*") || pattern.equals("*") || pattern.equals(".*")) + return "*/*"; // return any + else if (pattern.startsWith(".")) + return mimeTypeMap.getMimeTypeFromExtension(pattern.substring(1)); // remove leading '.' + else if (pattern.startsWith("*.")) + return mimeTypeMap.getMimeTypeFromExtension(pattern.substring(2)); // possibly null + else + return mimeTypeMap.getMimeTypeFromExtension(pattern); // possibly null + } + + private Uri getUriFromInitialPath(String initialPath) + { + // "Location should specify a document URI or a tree URI with document ID." + // https://developer.android.com/reference/android/provider/DocumentsContract.html#EXTRA_INITIAL_URI + if (initialPath.startsWith("content://")) + return Uri.parse(initialPath); + + return null; + } +} + + +/** + * An implementation of the text log + */ +class AllegroTextLog +{ + public void append(String tag, String message) + { + Log.i(tag, message); + } +} + + +/** + * Constants of the Native Dialog Addon + */ +class AllegroDialogConst +{ + // allegro_native_dialog.h + public static final int ALLEGRO_FILECHOOSER_FILE_MUST_EXIST = 1; + public static final int ALLEGRO_FILECHOOSER_SAVE = 2; + public static final int ALLEGRO_FILECHOOSER_FOLDER = 4; + public static final int ALLEGRO_FILECHOOSER_PICTURES = 8; + public static final int ALLEGRO_FILECHOOSER_SHOW_HIDDEN = 16; + public static final int ALLEGRO_FILECHOOSER_MULTIPLE = 32; + + public static final int ALLEGRO_MESSAGEBOX_WARN = 1; + public static final int ALLEGRO_MESSAGEBOX_ERROR = 2; + public static final int ALLEGRO_MESSAGEBOX_OK_CANCEL = 4; + public static final int ALLEGRO_MESSAGEBOX_YES_NO = 8; + public static final int ALLEGRO_MESSAGEBOX_QUESTION = 16; +} + + +/* vim: set sts=4 sw=4 et: */ diff --git a/docs/src/refman/native_dialog.txt b/docs/src/refman/native_dialog.txt index 130402adb0..7214e48632 100644 --- a/docs/src/refman/native_dialog.txt +++ b/docs/src/refman/native_dialog.txt @@ -109,6 +109,10 @@ call it from inside that thread. Returns true on success, false on failure. +> *Note:* On Android, [ALLEGRO_EVENT_DISPLAY_HALT_DRAWING] and +[ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING] need to be handled before this function +returns. This means that you must call it from a different thread. + ## API: al_get_native_file_dialog_count Returns the number of files selected, or 0 if the dialog was cancelled. @@ -118,6 +122,10 @@ Returns the number of files selected, or 0 if the dialog was cancelled. Returns one of the selected paths with index `i`. The index should range from `0` to the return value of [al_get_native_file_dialog_count] `-1`. +> *Note:* On Android, this function returns a content:// Universal Resource +Identifier instead of a file path due to the constraints of Scoped Storage. +Selected files may be accessed using [al_android_open_fd]. + ## API: al_destroy_native_file_dialog Frees up all resources used by the file dialog. @@ -133,7 +141,7 @@ dialog boxes usually have on the native system. If the `buttons` parameter is not NULL, you can instead specify the button text in a string, with buttons separated by a vertical bar (|). -> *Note:* `buttons` parameter is currently unimplemented on Windows. +> *Note:* The `buttons` parameter is currently unimplemented on Windows. The flags available are: @@ -209,6 +217,8 @@ ALLEGRO_TEXTLOG_MONOSPACE Returns NULL if there was an error opening the window, or if text log windows are not implemented on the platform. +> *Note:* On Android, logs can be viewed using logcat. + See also: [al_append_native_text_log], [al_close_native_text_log] ## API: al_close_native_text_log diff --git a/docs/src/refman/platform.txt b/docs/src/refman/platform.txt index 6920d5193e..fcaa39a232 100644 --- a/docs/src/refman/platform.txt +++ b/docs/src/refman/platform.txt @@ -156,6 +156,51 @@ Since: 5.2.2 > *[Unstable API]:* This API is new and subject to refinement. +### API: al_android_open_fd + +Opens a file descriptor to access data under a Universal Resource Identifier +(URI). This function accepts content:// and file:// URI schemes. You are +responsible for closing the returned file descriptor. + +The file `mode` can be `"r"`, `"w"`, `"rw"`, `"wt"`, `"wa"` or `"rwt"`. The +exact implementation of these modes differ depending on the underlying content +provider. For example, `"w"` may or may not truncate. + +Returns a file descriptor on success or a negative value on an error. On an +error, the Allegro errno is set. + +> *Note:* Remember to add to your manifest file the relevant permissions to +your app. + +Example: + +~~~~c +const char *content_uri = "content://..."; +int fd = al_android_open_fd(content_uri, "r"); + +if (fd >= 0) { + ALLEGRO_FILE *f = al_fopen_fd(fd, "r"); + + if (f != NULL) { + do_something_with(f); + al_fclose(f); + } + else { + handle_error(al_get_errno()); + close(fd); + } +} +else { + handle_error(al_get_errno()); +} +~~~~ + +Since: 5.2.10 + +See also: [al_fopen_fd], [al_get_errno] + +> *[Unstable API]:* This API is new and subject to refinement. + ## X11 These functions are declared in the following header file: diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4df2c8c44c..5fd79044f8 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -245,7 +245,7 @@ example(ex_stream_file CONSOLE ${AUDIO} ${ACODEC}) example(ex_stream_seek ${AUDIO} ${ACODEC} ${PRIM} ${FONT} ${IMAGE} ${DATA_IMAGES} ${DATA_AUDIO}) example(ex_synth ex_synth.cpp ${NIHGUI} ${AUDIO} ${TTF} DATA ${DATA_TTF}) -example(ex_native_filechooser ${DIALOG} ${FONT} ${IMAGE} ${COLOR}) +example(ex_native_filechooser ${DIALOG} ${FONT} ${IMAGE} ${COLOR} ${DATA_IMAGES}) example(ex_menu ${DIALOG} ${IMAGE}) # In some configurations CURL pulls in dependencies which we don't check for. diff --git a/examples/ex_native_filechooser.c b/examples/ex_native_filechooser.c index d8a2d358cb..a59d58c272 100644 --- a/examples/ex_native_filechooser.c +++ b/examples/ex_native_filechooser.c @@ -14,6 +14,10 @@ #include #include +#ifdef ALLEGRO_ANDROID +#include +#endif + #include "common.c" /* To communicate from a separate thread, we need a user event. */ @@ -166,6 +170,7 @@ int main(int argc, char **argv) AsyncDialog *cur_dialog = NULL; AsyncDialog *message_box = NULL; bool redraw = false; + bool halt_drawing = false; bool close_log = false; int button; bool message_log = true; @@ -213,6 +218,10 @@ int main(int argc, char **argv) } message("success.\n"); +#ifdef ALLEGRO_ANDROID + al_android_set_apk_file_interface(); +#endif + message("Loading font '%s'...", "data/fixed_font.tga"); font = al_load_font("data/fixed_font.tga", 0, 0); if (!font) { @@ -249,8 +258,11 @@ int main(int argc, char **argv) break; if (event.type == ALLEGRO_EVENT_KEY_DOWN) { - if (event.keyboard.keycode == ALLEGRO_KEY_ESCAPE && !cur_dialog) - break; + if (!cur_dialog) { + if (event.keyboard.keycode == ALLEGRO_KEY_ESCAPE || + event.keyboard.keycode == ALLEGRO_KEY_BACK) + break; + } } /* When a mouse button is pressed, and no native dialog is @@ -324,7 +336,28 @@ int main(int argc, char **argv) redraw = true; } - if (redraw && al_is_event_queue_empty(queue)) { +#ifdef ALLEGRO_ANDROID + if (event.type == ALLEGRO_EVENT_DISPLAY_HALT_DRAWING) { + message("Drawing halt"); + halt_drawing = true; + al_stop_timer(timer); + al_acknowledge_drawing_halt(display); + } + + if (event.type == ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING) { + message("Drawing resume"); + al_acknowledge_drawing_resume(display); + al_resume_timer(timer); + halt_drawing = false; + } + + if (event.type == ALLEGRO_EVENT_DISPLAY_RESIZE) { + message("Display resize"); + al_acknowledge_resize(display); + } +#endif + + if (redraw && !halt_drawing && al_is_event_queue_empty(queue)) { float x = al_get_display_width(display) / 2; float y = 0; redraw = false; diff --git a/include/allegro5/allegro_android.h b/include/allegro5/allegro_android.h index e8cf5c94d9..791eb13d4c 100644 --- a/include/allegro5/allegro_android.h +++ b/include/allegro5/allegro_android.h @@ -29,6 +29,7 @@ void al_android_set_apk_fs_interface(void); #if defined(ALLEGRO_UNSTABLE) || defined(ALLEGRO_INTERNAL_UNSTABLE) || defined(ALLEGRO_SRC) JNIEnv *al_android_get_jni_env(void); jobject al_android_get_activity(void); +int al_android_open_fd(const char *uri, const char *mode); #endif /* XXX decide if this should be public */ diff --git a/src/android/android_system.c b/src/android/android_system.c index 825250fcbf..2fc0cfab31 100644 --- a/src/android/android_system.c +++ b/src/android/android_system.c @@ -665,4 +665,27 @@ jobject al_android_get_activity(void) return _al_android_activity_object(); } +/* Function: al_android_open_fd + */ +int al_android_open_fd(const char *uri, const char *mode) +{ + JNIEnv *env = _al_android_get_jnienv(); + jobject activity = _al_android_activity_object(); + + jstring juri = _jni_call(env, jstring, NewStringUTF, uri != NULL ? uri : ""); /* content:// or file:// */ + jstring jmode = _jni_call(env, jstring, NewStringUTF, mode != NULL ? mode : ""); /* "r", "w", "rw", "wa", "wt", "rwt" */ + + int fd = _jni_callIntMethodV(env, activity, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I", juri, jmode); + + _jni_callv(env, DeleteLocalRef, jmode); + _jni_callv(env, DeleteLocalRef, juri); + + if (fd == -1) + al_set_errno(ENOENT); + else if (fd < 0) + al_set_errno(ENOTSUP); + + return fd; +} + /* vim: set sts=3 sw=3 et: */