From 6ef2b88d41ed3ef34856c4d75c5af15aaf21caa2 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Sun, 31 May 2026 23:00:12 +0300 Subject: [PATCH] Android can now share multiple files at once --- android-project/app/build.gradle | 4 +- android-project/app/proguard-rules.pro | 2 +- .../app/src/main/AndroidManifest.xml | 6 +- .../erroratline0/infinipaint/InfiniPaint.java | 52 ++++++---- src/AndroidJNICalls.cpp | 96 +++++++++++-------- src/AndroidJNICalls.hpp | 2 +- src/Screens/FileSelectScreen.cpp | 25 +++-- 7 files changed, 109 insertions(+), 78 deletions(-) diff --git a/android-project/app/build.gradle b/android-project/app/build.gradle index 59e4de1..021403a 100644 --- a/android-project/app/build.gradle +++ b/android-project/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId 'com.erroratline0.InfiniPaint' minSdkVersion 27 targetSdk 36 - versionCode 1 - versionName "0.5.0" + versionCode 2 + versionName "0.6.0" externalNativeBuild { cmake { cppFlags "-fexceptions", "-frtti" diff --git a/android-project/app/proguard-rules.pro b/android-project/app/proguard-rules.pro index db60cc6..ba24477 100644 --- a/android-project/app/proguard-rules.pro +++ b/android-project/app/proguard-rules.pro @@ -23,7 +23,7 @@ void startNetworkService(); void stopNetworkService(); void shareText(java.lang.String); - void shareInternalFile(java.lang.String, java.lang.String); + void shareInternalFiles(java.lang.String[], java.lang.String); } -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity { diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml index c98589a..1049b8e 100644 --- a/android-project/app/src/main/AndroidManifest.xml +++ b/android-project/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ @@ -82,7 +82,7 @@ --> 1) { + ArrayList arrayList = new ArrayList(); + for(String str : filePaths) { + File newFile = new File(getContext().getFilesDir(), str); + try { + arrayList.add(getUriForFile(getContext(), "com.erroratline0.infinipaint.fileprovider", newFile)); + } catch (Exception e) { + Log.v("INFO", "[shareInternalFile] Exception " + e); + return; + } + } + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + sendIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, arrayList); + sendIntent.setType(mimeType); + Intent shareIntent = Intent.createChooser(sendIntent, null); + getContext().startActivity(shareIntent); + } } static public void shareText(String textToSend) { diff --git a/src/AndroidJNICalls.cpp b/src/AndroidJNICalls.cpp index eeb3858..5d660c3 100644 --- a/src/AndroidJNICalls.cpp +++ b/src/AndroidJNICalls.cpp @@ -28,6 +28,23 @@ #include #include "NetThreadManager.hpp" +template class JNILocalRef { + public: + JNILocalRef(JNIEnv* e, T r): + env(e), + ref(r) {} + ~JNILocalRef() { + env->DeleteLocalRef(ref); + } + T get() const { return ref; } + operator T() const { return ref; } + JNILocalRef(const JNILocalRef&) = delete; + JNILocalRef& operator=(const JNILocalRef&) = delete; + private: + JNIEnv* env; + T ref; +}; + std::string jstring2string(JNIEnv *env, jstring jStr) { if (!jStr) return ""; @@ -42,9 +59,9 @@ std::string jstring2string(JNIEnv *env, jstring jStr) { return std::string(str.c_str(), str.size()); } -jstring string2jstring(JNIEnv* env, const std::string& s) { +JNILocalRef string2jstring(JNIEnv* env, const std::string& s) { std::u16string u16str = SkUnicodes::ICU::Make()->convertUtf8ToUtf16(s.c_str(), s.size()); - return env->NewString((const jchar*)u16str.data(), u16str.length()); + return JNILocalRef(env, env->NewString((const jchar*)u16str.data(), u16str.length())); } namespace AndroidJNICalls { @@ -58,26 +75,31 @@ namespace AndroidJNICalls { bool textChanged = false; bool cursorChanged = false; - void shareInternalFile(const std::string& filePath, const std::string& mimeType) { - Logger::get().log(Logger::LogType::INFO, "[AndroidJNICalls::shareInternalFile] Share internal file " + filePath); + void shareInternalFiles(const std::vector& filePaths, const std::string& mimeType) { + if(filePaths.empty()) + return; + Logger::get().log(Logger::LogType::INFO, "[AndroidJNICalls::shareInternalFile] Share internal files"); JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); - jobject activity = (jobject)SDL_GetAndroidActivity(); - jclass clazz = env->GetObjectClass(activity); - jmethodID method_id = env->GetStaticMethodID(clazz, "shareInternalFile", "(Ljava/lang/String;Ljava/lang/String;)V"); - env->CallStaticVoidMethod(clazz, method_id, string2jstring(env, filePath), string2jstring(env, mimeType)); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); + JNILocalRef activity(env, (jobject)SDL_GetAndroidActivity()); + JNILocalRef clazz(env, env->GetObjectClass(activity)); + JNILocalRef jStrClass(env, env->FindClass("java/lang/String")); + JNILocalRef jFilePathArray(env, env->NewObjectArray(filePaths.size(), jStrClass, nullptr)); + for(size_t i = 0; i < filePaths.size(); i++) { + JNILocalRef s = string2jstring(env, filePaths[i]); + env->SetObjectArrayElement(jFilePathArray, i, s); + } + + jmethodID method_id = env->GetStaticMethodID(clazz.get(), "shareInternalFiles", "([Ljava/lang/String;Ljava/lang/String;)V"); + env->CallStaticVoidMethod(clazz, method_id, jFilePathArray.get(), string2jstring(env, mimeType).get()); } void shareText(const std::string& str) { Logger::get().log(Logger::LogType::INFO, "[AndroidJNICalls::shareText] Share text " + str); JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); - jobject activity = (jobject)SDL_GetAndroidActivity(); - jclass clazz = env->GetObjectClass(activity); - jmethodID method_id = env->GetStaticMethodID(clazz, "shareText", "(Ljava/lang/String;)V"); - env->CallStaticVoidMethod(clazz, method_id, string2jstring(env, str)); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); + JNILocalRef activity(env, (jobject)SDL_GetAndroidActivity()); + JNILocalRef clazz(env, env->GetObjectClass(activity)); + jmethodID method_id = env->GetStaticMethodID(clazz.get(), "shareText", "(Ljava/lang/String;)V"); + env->CallStaticVoidMethod(clazz, method_id, string2jstring(env, str).get()); } int get_android_text_pos_from_cursor_pos(const std::shared_ptr& t, const RichText::TextPosition& p) { @@ -108,36 +130,30 @@ namespace AndroidJNICalls { modMap = nullptr; JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); - jobject activity = (jobject)SDL_GetAndroidActivity(); - jclass clazz = env->GetObjectClass(activity); - jmethodID method_id = env->GetStaticMethodID(clazz, "startTextInput", "(JLjava/lang/String;III)V"); - env->CallStaticVoidMethod(clazz, method_id, textboxID, string2jstring(env, textBox->get_string()), + JNILocalRef activity(env, (jobject)SDL_GetAndroidActivity()); + JNILocalRef clazz(env, env->GetObjectClass(activity)); + jmethodID method_id = env->GetStaticMethodID(clazz.get(), "startTextInput", "(JLjava/lang/String;III)V"); + env->CallStaticVoidMethod(clazz, method_id, textboxID, string2jstring(env, textBox->get_string()).get(), get_android_text_pos_from_cursor_pos(textBox, std::min(cursor->selectionBeginPos, cursor->selectionEndPos)), get_android_text_pos_from_cursor_pos(textBox, std::max(cursor->selectionBeginPos, cursor->selectionEndPos)), inputType); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); } void startNetworkService() { Logger::get().log(Logger::LogType::INFO, "[AndroidJNICalls::startNetworkService] Start service"); JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); - jobject activity = (jobject)SDL_GetAndroidActivity(); - jclass clazz = env->GetObjectClass(activity); - jmethodID method_id = env->GetStaticMethodID(clazz, "startNetworkService", "()V"); + JNILocalRef activity(env, (jobject)SDL_GetAndroidActivity()); + JNILocalRef clazz(env, env->GetObjectClass(activity)); + jmethodID method_id = env->GetStaticMethodID(clazz.get(), "startNetworkService", "()V"); env->CallStaticVoidMethod(clazz, method_id); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); } void stopNetworkService() { Logger::get().log(Logger::LogType::INFO, "[AndroidJNICalls::stopNetworkService] Stop service"); JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); - jobject activity = (jobject)SDL_GetAndroidActivity(); - jclass clazz = env->GetObjectClass(activity); - jmethodID method_id = env->GetStaticMethodID(clazz, "stopNetworkService", "()V"); + JNILocalRef activity(env, (jobject)SDL_GetAndroidActivity()); + JNILocalRef clazz(env, env->GetObjectClass(activity)); + jmethodID method_id = env->GetStaticMethodID(clazz.get(), "stopNetworkService", "()V"); env->CallStaticVoidMethod(clazz, method_id); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); } void updateModMap(CustomEvents::InputTextBoxID tId, const std::shared_ptr& newModMap) { @@ -153,14 +169,12 @@ namespace AndroidJNICalls { *cursor = *newCursor; JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); - jobject activity = (jobject)SDL_GetAndroidActivity(); - jclass clazz = env->GetObjectClass(activity); - jmethodID method_id = env->GetStaticMethodID(clazz, "updateCursorPos", "(II)V"); + JNILocalRef activity(env, (jobject)SDL_GetAndroidActivity()); + JNILocalRef clazz(env, env->GetObjectClass(activity)); + jmethodID method_id = env->GetStaticMethodID(clazz.get(), "updateCursorPos", "(II)V"); env->CallStaticVoidMethod(clazz, method_id, get_android_text_pos_from_cursor_pos(textBox, std::min(cursor->selectionBeginPos, cursor->selectionEndPos)), get_android_text_pos_from_cursor_pos(textBox, std::max(cursor->selectionBeginPos, cursor->selectionEndPos))); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); } } @@ -172,14 +186,12 @@ namespace AndroidJNICalls { textBox->set_rich_text_data(newTextbox->get_rich_text_data()); JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); - jobject activity = (jobject)SDL_GetAndroidActivity(); - jclass clazz = env->GetObjectClass(activity); - jmethodID method_id = env->GetStaticMethodID(clazz, "updateCursorPos", "(II)V"); + JNILocalRef activity(env, (jobject)SDL_GetAndroidActivity()); + JNILocalRef clazz(env, env->GetObjectClass(activity)); + jmethodID method_id = env->GetStaticMethodID(clazz.get(), "updateCursorPos", "(II)V"); env->CallStaticVoidMethod(clazz, method_id, get_android_text_pos_from_cursor_pos(textBox, std::min(cursor->selectionBeginPos, cursor->selectionEndPos)), get_android_text_pos_from_cursor_pos(textBox, std::max(cursor->selectionBeginPos, cursor->selectionEndPos))); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); } } diff --git a/src/AndroidJNICalls.hpp b/src/AndroidJNICalls.hpp index f0170c2..3518ef0 100644 --- a/src/AndroidJNICalls.hpp +++ b/src/AndroidJNICalls.hpp @@ -26,7 +26,7 @@ struct InputManager; namespace AndroidJNICalls { extern InputManager* globalInputManager; - void shareInternalFile(const std::string& filePath, const std::string& mimeType); + void shareInternalFiles(const std::vector& filePaths, const std::string& mimeType); void shareText(const std::string& str); void startTextInput(CustomEvents::InputTextBoxID newTextboxID, const std::shared_ptr& newTextbox, const std::shared_ptr& newCursor, const std::shared_ptr& modMap, int inputType); void startNetworkService(); diff --git a/src/Screens/FileSelectScreen.cpp b/src/Screens/FileSelectScreen.cpp index d8b1068..49420a0 100644 --- a/src/Screens/FileSelectScreen.cpp +++ b/src/Screens/FileSelectScreen.cpp @@ -428,14 +428,15 @@ void FileSelectScreen::move_selected_files(const std::filesystem::path& fromPath } void FileSelectScreen::share_selected_files() { - for(const FileInfo& f : fileList) { - if(f.selected) { #ifdef __ANDROID__ - AndroidJNICalls::shareInternalFile("saves/" + f.fileName + ".infpnt", "application/octet-stream"); -#endif - break; - } + std::vector filesToSend; + for(const FileInfo& f : fileList) { + if(f.selected) + filesToSend.emplace_back("saves/" + f.fileName + ".infpnt"); } + if(!filesToSend.empty()) + AndroidJNICalls::shareInternalFiles(filesToSend, "application/octet-stream"); +#endif } void FileSelectScreen::duplicate_selected_files(const std::filesystem::path& inPath) { @@ -583,13 +584,11 @@ void FileSelectScreen::edit_action_bar() { editMode = false; }); #ifdef __ANDROID__ - if(numberOfSelectedEntries == 1) { - edit_action_bar_button("share", "data/icons/RemixIcon/share-line.svg", "Share", [&] { - share_selected_files(); - update_file_list(fileList, savePath, false); - editMode = false; - }); - } + edit_action_bar_button("share", "data/icons/RemixIcon/share-line.svg", "Share", [&] { + share_selected_files(); + update_file_list(fileList, savePath, false); + editMode = false; + }); #endif } else if(selectedMenu == SelectedMenu::TRASH) {