diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index e53354dc9..5bf5611c9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -689,6 +689,19 @@ object NativeLibrary { FileUtil.renameFile(path, destinationFilename) } + @Keep + @JvmStatic + fun moveFile(filename: String, sourceDirPath: String, destinationDirPath: String): Boolean = + if (FileUtil.isNativePath(sourceDirPath)) { + try { + CitraApplication.documentsTree.moveFile(filename, sourceDirPath, destinationDirPath) + } catch (e: Exception) { + false + } + } else { + FileUtil.moveFile(filename, sourceDirPath, destinationDirPath) + } + @Keep @JvmStatic fun deleteDocument(path: String): Boolean = diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DocumentsTree.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/DocumentsTree.kt index e2b015d47..debdcff03 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/DocumentsTree.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DocumentsTree.kt @@ -191,7 +191,7 @@ class DocumentsTree { } @Synchronized - fun renameFile(filepath: String, destinationFilename: String?): Boolean { + fun renameFile(filepath: String, destinationFilename: String): Boolean { val node = resolvePath(filepath) ?: return false try { val filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD) @@ -203,6 +203,20 @@ class DocumentsTree { } } + @Synchronized + fun moveFile(filename: String, sourceDirPath: String, destDirPath: String): Boolean { + val sourceFileNode = resolvePath(sourceDirPath + "/" + filename) ?: return false + val sourceDirNode = resolvePath(sourceDirPath) ?: return false + val destDirNode = resolvePath(destDirPath) ?: return false + try { + val newUri = DocumentsContract.moveDocument(context.contentResolver, sourceFileNode.uri!!, sourceDirNode.uri!!, destDirNode.uri!!) + sourceFileNode.rename(filename, newUri) + return true + } catch (e: Exception) { + error("[DocumentsTree]: Cannot move file, error: " + e.message) + } + } + @Synchronized fun deleteDocument(filepath: String): Boolean { val node = resolvePath(filepath) ?: return false diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt index 402a23857..9d9063c59 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -11,6 +11,7 @@ import android.net.Uri import android.provider.DocumentsContract import android.system.Os import android.util.Pair +import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.model.CheapDocument @@ -434,6 +435,20 @@ object FileUtil { return false } + @JvmStatic + fun moveFile(filename: String, sourceDirUriString: String, destDirUriString: String): Boolean { + try { + val sourceFileUri = ("$sourceDirUriString%2F$filename").toUri() + val sourceDirUri = sourceDirUriString.toUri() + val destDirUri = destDirUriString.toUri() + DocumentsContract.moveDocument(context.contentResolver, sourceFileUri, sourceDirUri, destDirUri) + return true + } catch (e: Exception) { + Log.error("[FileUtil]: Cannot move file, error: " + e.message) + } + return false + } + @JvmStatic fun deleteDocument(path: String): Boolean { try { diff --git a/src/common/android_storage.cpp b/src/common/android_storage.cpp index a18ecefac..3da5763da 100644 --- a/src/common/android_storage.cpp +++ b/src/common/android_storage.cpp @@ -1,9 +1,10 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #ifdef ANDROID #include "common/android_storage.h" +#include "common/file_util.h" namespace AndroidStorage { JNIEnv* GetEnvForThread() { @@ -172,6 +173,32 @@ bool RenameFile(const std::string& source, const std::string& filename) { j_destination_path); } +bool MoveFile(const std::string& filename, const std::string& source_dir_path, + const std::string& destination_dir_path) { + if (move_file == nullptr) + return false; + auto env = GetEnvForThread(); + jstring j_filename = env->NewStringUTF(filename.c_str()); + jstring j_source_dir_path = env->NewStringUTF(source_dir_path.c_str()); + jstring j_destination_dir_path = env->NewStringUTF(destination_dir_path.c_str()); + return env->CallStaticBooleanMethod(native_library, move_file, j_filename, j_source_dir_path, + j_destination_dir_path); +} + +bool MoveAndRenameFile(const std::string& src_full_path, const std::string& dest_full_path) { + const auto src_filename = std::string(FileUtil::GetFilename(src_full_path)); + const auto src_parent_path = std::string(FileUtil::GetParentPath(src_full_path)); + const auto dest_filename = std::string(FileUtil::GetFilename(dest_full_path)); + const auto dest_parent_path = std::string(FileUtil::GetParentPath(dest_full_path)); + + bool result = AndroidStorage::MoveFile(src_filename, src_parent_path, dest_parent_path); + if (result == false) { + return false; + } + result = AndroidStorage::RenameFile((dest_parent_path + src_filename), dest_filename); + return result; +} + #define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \ F(FunctionName, ReturnValue, JMethodID, Caller) #define F(FunctionName, ReturnValue, JMethodID, Caller) \ diff --git a/src/common/android_storage.h b/src/common/android_storage.h index 2ea0eb57c..f3538f018 100644 --- a/src/common/android_storage.h +++ b/src/common/android_storage.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -24,7 +24,11 @@ const std::string& destination_filename), \ copy_file, "copyFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z") \ V(RenameFile, bool, (const std::string& source, const std::string& filename), rename_file, \ - "renameFile", "(Ljava/lang/String;Ljava/lang/String;)Z") + "renameFile", "(Ljava/lang/String;Ljava/lang/String;)Z") \ + V(MoveFile, bool, \ + (const std::string& filename, const std::string& source_dir_path, \ + const std::string& destination_dir_path), \ + move_file, "moveFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z") #define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \ V(IsDirectory, bool, is_directory, CallStaticBooleanMethod, "isDirectory", \ "(Ljava/lang/String;)Z") \ @@ -44,6 +48,7 @@ ANDROID_STORAGE_FUNCTIONS(FS) #undef F #undef FS #undef FR +bool MoveAndRenameFile(const std::string& src_full_path, const std::string& dest_full_path); // Reference: // https://developer.android.com/reference/android/os/ParcelFileDescriptor#parseMode(java.lang.String) enum class AndroidOpenMode { diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index c740e8416..12a04863f 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -304,20 +304,20 @@ bool DeleteDir(const std::string& filename) { return false; } -bool Rename(const std::string& srcFilename, const std::string& destFilename) { - LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename); +bool Rename(const std::string& srcFullPath, const std::string& destFullPath) { + LOG_TRACE(Common_Filesystem, "{} --> {}", srcFullPath, destFullPath); #ifdef _WIN32 - if (_wrename(Common::UTF8ToUTF16W(srcFilename).c_str(), - Common::UTF8ToUTF16W(destFilename).c_str()) == 0) + if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(), + Common::UTF8ToUTF16W(destFullPath).c_str()) == 0) return true; #elif ANDROID - if (AndroidStorage::RenameFile(srcFilename, std::string(GetFilename(destFilename)))) + if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath)) return true; #else - if (rename(srcFilename.c_str(), destFilename.c_str()) == 0) + if (rename(srcFullPath.c_str(), destFullPath.c_str()) == 0) return true; #endif - LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename, + LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFullPath, destFullPath, GetLastErrorMsg()); return false; } diff --git a/src/common/file_util.h b/src/common/file_util.h index 57a1d67e1..1fa4d1f5d 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -136,13 +136,13 @@ bool Delete(const std::string& filename); // Deletes a directory filename, returns true on success bool DeleteDir(const std::string& filename); -// renames file srcFilename to destFilename, returns true on success -bool Rename(const std::string& srcFilename, const std::string& destFilename); +// Renames file srcFullPath to destFullPath, returns true on success +bool Rename(const std::string& srcFullPath, const std::string& destFullPath); -// copies file srcFilename to destFilename, returns true on success +// Copies file srcFilename to destFilename, returns true on success bool Copy(const std::string& srcFilename, const std::string& destFilename); -// creates an empty file filename, returns true on success +// Creates an empty file filename, returns true on success bool CreateEmptyFile(const std::string& filename); /**