From 49fa854c933cc4a7b93ef9c73cacc75b7e5e2890 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Wed, 23 Apr 2025 18:13:15 +0200 Subject: [PATCH] add a media ContentProvider Every time the ContentProvider is accessed, a file chooser opens and the selected file is then provided as media file. --- meson.build | 1 + .../android_content_ATLMediaContentProvider.c | 25 +++ .../android_content_ATLMediaContentProvider.h | 21 +++ .../content/ATLMediaContentProvider.java | 145 ++++++++++++++++++ .../android/content/ContentProvider.java | 1 + src/api-impl/android/provider/MediaStore.java | 44 ++++++ src/api-impl/meson.build | 1 + 7 files changed, 238 insertions(+) create mode 100644 src/api-impl-jni/content/android_content_ATLMediaContentProvider.c create mode 100644 src/api-impl-jni/generated_headers/android_content_ATLMediaContentProvider.h create mode 100644 src/api-impl/android/content/ATLMediaContentProvider.java diff --git a/meson.build b/meson.build index 7247681d..938f9ae1 100644 --- a/meson.build +++ b/meson.build @@ -113,6 +113,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/AssetInputStream.c', 'src/api-impl-jni/audio/android_media_AudioTrack.c', 'src/api-impl-jni/audio/android_media_SoundPool.c', + 'src/api-impl-jni/content/android_content_ATLMediaContentProvider.c', 'src/api-impl-jni/content/android_content_ClipboardManager.c', 'src/api-impl-jni/content/android_content_ContentResolver.c', 'src/api-impl-jni/content/android_content_Context.c', diff --git a/src/api-impl-jni/content/android_content_ATLMediaContentProvider.c b/src/api-impl-jni/content/android_content_ATLMediaContentProvider.c new file mode 100644 index 00000000..a5066d06 --- /dev/null +++ b/src/api-impl-jni/content/android_content_ATLMediaContentProvider.c @@ -0,0 +1,25 @@ +#include +#include + +#include "../generated_headers/android_content_ATLMediaContentProvider.h" +#include "../defines.h" +#include "../util.h" + +extern GtkWindow *window; + +static void file_dialog_callback(GObject *dialog, GAsyncResult *res, gpointer user_data) { + GFile *file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(dialog), res, NULL); + JNIEnv *env = get_jni_env(); + jobject this = (jobject) user_data; + (*env)->CallVoidMethod(env, this, _METHOD(_CLASS(this), "setSelectedFile", "(Ljava/lang/String;)V"), _JSTRING(g_file_get_path(file))); + g_object_unref(file); + _UNREF(this); +} + +JNIEXPORT void JNICALL Java_android_content_ATLMediaContentProvider_native_1open_1media_1folder(JNIEnv *env, jobject this) +{ + GtkFileDialog *dialog = gtk_file_dialog_new(); + gtk_file_dialog_set_title(GTK_FILE_DIALOG(dialog), "Open Media Folder"); + gtk_file_dialog_set_modal(GTK_FILE_DIALOG(dialog), TRUE); + gtk_file_dialog_open(dialog, window, NULL, file_dialog_callback, _REF(this)); +} diff --git a/src/api-impl-jni/generated_headers/android_content_ATLMediaContentProvider.h b/src/api-impl-jni/generated_headers/android_content_ATLMediaContentProvider.h new file mode 100644 index 00000000..7c3e84ca --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_content_ATLMediaContentProvider.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_content_ATLMediaContentProvider */ + +#ifndef _Included_android_content_ATLMediaContentProvider +#define _Included_android_content_ATLMediaContentProvider +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: android_content_ATLMediaContentProvider + * Method: native_open_media_folder + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_android_content_ATLMediaContentProvider_native_1open_1media_1folder + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl/android/content/ATLMediaContentProvider.java b/src/api-impl/android/content/ATLMediaContentProvider.java new file mode 100644 index 00000000..52875f46 --- /dev/null +++ b/src/api-impl/android/content/ATLMediaContentProvider.java @@ -0,0 +1,145 @@ +package android.content; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; + +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; + +public class ATLMediaContentProvider extends ContentProvider { + + boolean waitingForFileChooser = false; + File selectedFile = null; + long timestamp = 0; + + // called from native + void setSelectedFile(String selectedFile) { + this.selectedFile = new File(selectedFile); + this.waitingForFileChooser = false; + this.timestamp = System.currentTimeMillis(); + synchronized(this) { + notifyAll(); + } + } + + private void openFileChooser() { + if (!waitingForFileChooser) { + waitingForFileChooser = true; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + native_open_media_folder(); + } + }); + } + synchronized(this) { + try { + while (waitingForFileChooser) { + wait(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + // if we haven't selected a file, open the file chooser + if (!"0".equals(uri.getLastPathSegment()) && timestamp + 1000 < System.currentTimeMillis()) { + openFileChooser(); + } + MatrixCursor cursor = new MatrixCursor(projection); + Object[] row = new Object[projection.length]; + if (uri.getQueryParameter("distinct") != null) { + for (int i = 0; i < projection.length; i++) { + switch (projection[i]) { + case "bucket_display_name": + row[i] = "files"; + break; + case "bucket_id": + row[i] = 0; + break; + } + } + } else { + for (int i = 0; i < projection.length; i++) { + switch (projection[i]) { + case "_id": + row[i] = 0; + break; + case "_data": + case "title": + row[i] = selectedFile; + break; + case "mime_type": + row[i] = getType(uri); + break; + case "media_type": + if (getType(uri).startsWith("image/")) + row[i] = 1; + else if (getType(uri).startsWith("audio/")) + row[i] = 2; + else if (getType(uri).startsWith("video/")) + row[i] = 3; + else + row[i] = 0; + break; + case "date_modified": + case "datetaken": + row[i] = selectedFile.lastModified(); + break; + case "orientation": + row[i] = 0; + break; + case "_size": + row[i] = selectedFile.length(); + break; + } + } + } + cursor.addRow(row); + return cursor; + } + + @Override + public String getType(Uri uri) { + try { + return Files.probeContentType(selectedFile.toPath()); + } catch (IOException e) { + e.printStackTrace(); + return "application/octet-stream"; + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + return ParcelFileDescriptor.open(selectedFile, ParcelFileDescriptor.parseMode(mode)); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'insert'"); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'update'"); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'delete'"); + } + + private native void native_open_media_folder(); +} diff --git a/src/api-impl/android/content/ContentProvider.java b/src/api-impl/android/content/ContentProvider.java index 73143b18..1f9e8cb1 100644 --- a/src/api-impl/android/content/ContentProvider.java +++ b/src/api-impl/android/content/ContentProvider.java @@ -34,6 +34,7 @@ public abstract class ContentProvider { providers.put(provider_parsed.info.authority, provider); } catch(Exception e) { e.printStackTrace(); } } + providers.put("media", new ATLMediaContentProvider()); } public boolean onCreate() {return false;} diff --git a/src/api-impl/android/provider/MediaStore.java b/src/api-impl/android/provider/MediaStore.java index ed145721..e1acb10e 100644 --- a/src/api-impl/android/provider/MediaStore.java +++ b/src/api-impl/android/provider/MediaStore.java @@ -1,6 +1,13 @@ package android.provider; +import java.io.FileNotFoundException; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.ParcelFileDescriptor; public class MediaStore { @@ -9,6 +16,43 @@ public class MediaStore { public static class Media { public static final Uri EXTERNAL_CONTENT_URI = Uri.parse("content://media/external/images/media"); + public static final Uri INTERNAL_CONTENT_URI = Uri.parse("content://media/internal/images/media"); + } + + public static class Thumbnails { + + public static Cursor queryMiniThumbnail(ContentResolver contentResolver, long id, int kind, String[] projection) { + return null; + } + + public static Bitmap getThumbnail(ContentResolver contentResolver, long imageId, long groupId, int kind, BitmapFactory.Options options) throws FileNotFoundException { + ParcelFileDescriptor fd = contentResolver.openFileDescriptor(Media.EXTERNAL_CONTENT_URI.buildUpon().appendPath(String.valueOf(imageId)).build(), "r"); + return BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, options); + } + } + } + + public static class Video { + + public static class Media { + + public static final Uri EXTERNAL_CONTENT_URI = Uri.parse("content://media/external/video/media"); + public static final Uri INTERNAL_CONTENT_URI = Uri.parse("content://media/internal/video/media"); + } + } + + public static class Audio { + + public static class Media { + + public static final Uri EXTERNAL_CONTENT_URI = Uri.parse("content://media/external/audio/media"); + } + } + + public static class Files { + + public static Uri getContentUri(String type) { + return Uri.parse("content://media/files/" + type); } } } diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 41bec462..c6458a4b 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -76,6 +76,7 @@ srcs = [ 'android/bluetooth/le/ScanCallback.java', 'android/content/ActivityNotFoundException.java', 'android/content/AsyncQueryHandler.java', + 'android/content/ATLMediaContentProvider.java', 'android/content/BroadcastReceiver.java', 'android/content/ClipboardManager.java', 'android/content/ClipData.java',