diff --git a/app/src/main/cpp/Core/fileselector/fileselector.cpp b/app/src/main/cpp/Core/fileselector/fileselector.cpp index e0dbd8b..1c0aee0 100644 --- a/app/src/main/cpp/Core/fileselector/fileselector.cpp +++ b/app/src/main/cpp/Core/fileselector/fileselector.cpp @@ -12,6 +12,7 @@ jclass class_FileSelector; jclass class_String; jmethodID method_nselectFile; jmethodID method_nselectFileMulti; +jmethodID method_nselectFileMultiNamed; jmethodID method_nselectFiles; @@ -20,6 +21,7 @@ PRIVATE_API void fsel_setup(JNIEnv* env) { class_String = (jclass)env->NewGlobalRef(env->FindClass("java/lang/String")); method_nselectFile = env->GetStaticMethodID(class_FileSelector, "nselectFile", "(Ljava/lang/String;JZ)Z"); method_nselectFileMulti = env->GetStaticMethodID(class_FileSelector, "nselectFileMulti", "([Ljava/lang/String;JZ)Z"); + method_nselectFileMultiNamed = env->GetStaticMethodID(class_FileSelector, "nselectFileMultiNamed", "([Ljava/lang/String;JZLjava/lang/String;)Z"); method_nselectFiles = env->GetStaticMethodID(class_FileSelector, "nselectFiles", "([Ljava/lang/String;J)Z"); } extern "C" @@ -91,8 +93,9 @@ static jobjectArray fsel_build_mime_array(JNIEnv* env, return arr; } -bool requestFileMulti(const char* const* mime_types, size_t mime_type_count, - callback_function callback, bool save) { +bool requestFileMultiNamed(const char* const* mime_types, size_t mime_type_count, + callback_function callback, bool save, + const char* suggested_name) { if (!mime_types || mime_type_count == 0) return false; JNIEnv* env; @@ -104,17 +107,38 @@ bool requestFileMulti(const char* const* mime_types, size_t mime_type_count, jobjectArray mimeArr = fsel_build_mime_array(env, mime_types, mime_type_count); if (!mimeArr) return false; + // Null stays null all the way into the Java side, where it means "leave the + // name field as it was" - the pre-EXTRA_TITLE behaviour, and what + // requestFileMulti below still asks for. + jstring nameStr = nullptr; + if (suggested_name && suggested_name[0] != '\0') { + nameStr = env->NewStringUTF(suggested_name); + if (!nameStr || fsel_check_exception(env, "NewStringUTF(name)")) { + env->DeleteLocalRef(mimeArr); + return false; + } + } + jboolean result = env->CallStaticBooleanMethod( - class_FileSelector, method_nselectFileMulti, - mimeArr, (jlong)callback, save); + class_FileSelector, method_nselectFileMultiNamed, + mimeArr, (jlong)callback, save, nameStr); const bool exceptionRaised = fsel_check_exception(env, "CallStaticBooleanMethod"); + if (nameStr) env->DeleteLocalRef(nameStr); env->DeleteLocalRef(mimeArr); if (exceptionRaised) return false; return result == JNI_TRUE; } +// Kept as a delegate rather than a second copy of the body: this symbol is +// linked by every mod already built against this header, so it has to stay - +// but two JNI bodies to keep in step is how the two forms drift apart. +bool requestFileMulti(const char* const* mime_types, size_t mime_type_count, + callback_function callback, bool save) { + return requestFileMultiNamed(mime_types, mime_type_count, callback, save, nullptr); +} + bool requestFiles(const char* const* mime_types, size_t mime_type_count, callback_function_files callback) { if (!mime_types || mime_type_count == 0) return false; diff --git a/app/src/main/cpp/Core/fileselector/fileselector.h b/app/src/main/cpp/Core/fileselector/fileselector.h index 464f679..ef4f745 100644 --- a/app/src/main/cpp/Core/fileselector/fileselector.h +++ b/app/src/main/cpp/Core/fileselector/fileselector.h @@ -44,6 +44,29 @@ bool requestFileMulti(const char* const* mime_types, callback_function callback, bool save); +/** + * requestFileMulti, plus a proposed file name for save mode. + * suggested_name - initial name shown in the picker, or nullptr/"" for none. + * Ignored when save is false; the user can always change it. + * + * Pass this whenever save is true. ACTION_CREATE_DOCUMENT takes its initial + * name from EXTRA_TITLE, and with no name the picker's field opens empty - which + * the platform does not leave empty: an empty display name comes back as the + * literal "(invalid)" from FileUtils.buildValidFatFilename, so the file is + * created as "(invalid).". + * + * Include the extension. FileUtils.splitFileName keeps it when it maps back to + * mime_types[0] and swaps it for the type's own otherwise, so there is no + * double-suffix case to avoid. Illegal characters, over-long names and + * collisions are all handled by the provider - it appends "(1)" rather than + * overwriting. + */ +bool requestFileMultiNamed(const char* const* mime_types, + size_t mime_type_count, + callback_function callback, + bool save, + const char* suggested_name); + /** * Callback for requestFiles. * count - number of file descriptors; 0 on cancel or all-failed diff --git a/app/src/main/java/git/artdeell/skymodloader/FileSelector.java b/app/src/main/java/git/artdeell/skymodloader/FileSelector.java index ebc7633..3d9a8bb 100644 --- a/app/src/main/java/git/artdeell/skymodloader/FileSelector.java +++ b/app/src/main/java/git/artdeell/skymodloader/FileSelector.java @@ -46,7 +46,26 @@ public boolean selectFile(String mimeType, long callbackFunction, boolean save) public static boolean nselectFileMulti(String[] mimeTypes, long callbackFunction, boolean save) { return selector.selectFileMulti(mimeTypes, callbackFunction, save); } + + // Save mode with a proposed file name. + // + // Kept as a SEPARATE entry point rather than a fourth argument on the one + // above: nselectFileMulti is resolved from native by its exact signature + // ([Ljava/lang/String;JZ)Z, so every mod built against the old Canvas would + // fail to find it the moment that changed. + public static boolean nselectFileMultiNamed(String[] mimeTypes, long callbackFunction, + boolean save, String suggestedName) { + return selector.selectFileMulti(mimeTypes, callbackFunction, save, suggestedName); + } + public boolean selectFileMulti(String[] mimeTypes, long callbackFunction, boolean save) { + return selectFileMulti(mimeTypes, callbackFunction, save, null); + } + + // suggestedName - initial name for the save picker, or null to leave the + // field empty as before. May include the extension; see buildPickerIntent. + public boolean selectFileMulti(String[] mimeTypes, long callbackFunction, boolean save, + String suggestedName) { if(this.callbackFunction != 0) return false; if(mimeTypes == null || mimeTypes.length == 0) return false; @@ -65,7 +84,7 @@ public boolean selectFileMulti(String[] mimeTypes, long callbackFunction, boolea this.isSaveMode = save; this.isMultiFilesMode = false; gameActivity.startActivityForResult( - buildPickerIntent(types, save, /*allowMultiple=*/false), + buildPickerIntent(types, save, /*allowMultiple=*/false, suggestedName), SELECTING_FILE); }); return true; @@ -85,13 +104,15 @@ public boolean selectFiles(String[] mimeTypes, long callbackFunction) { this.isSaveMode = false; this.isMultiFilesMode = true; gameActivity.startActivityForResult( - buildPickerIntent(types, /*save=*/false, /*allowMultiple=*/true), + buildPickerIntent(types, /*save=*/false, /*allowMultiple=*/true, + /*suggestedName=*/null), SELECTING_FILE); }); return true; } - private static Intent buildPickerIntent(String[] types, boolean save, boolean allowMultiple) { + private static Intent buildPickerIntent(String[] types, boolean save, boolean allowMultiple, + String suggestedName) { Intent i = new Intent(save ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); if(allowMultiple) i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); @@ -101,6 +122,22 @@ private static Intent buildPickerIntent(String[] types, boolean save, boolean al i.setType("*/*"); i.putExtra(Intent.EXTRA_MIME_TYPES, types); } + // ACTION_CREATE_DOCUMENT reads its initial file name from EXTRA_TITLE; + // the user can still change it before saving. Without it the name field + // opens empty, and an empty display name is NOT left empty by the + // platform - FileUtils.buildValidFatFilename substitutes the literal + // "(invalid)", so saved files arrive as "(invalid).json". + // + // The name may carry its extension. FileUtils.splitFileName keeps the + // caller's extension when it maps back to the requested MIME type and + // replaces it otherwise, so "sheet.json" + "application/json" stays + // "sheet.json" rather than gaining a second suffix. Illegal characters + // and over-long names are sanitized by the provider, so nothing here has + // to - only the empty case needs guarding, since that is the one + // buildValidFatFilename cannot rescue. + if(save && suggestedName != null && !suggestedName.isEmpty()) { + i.putExtra(Intent.EXTRA_TITLE, suggestedName); + } return i; }