diff --git a/.ci/android.sh b/.ci/android.sh index db3dac419..fc945e243 100755 --- a/.ci/android.sh +++ b/.ci/android.sh @@ -9,8 +9,14 @@ fi cd src/android chmod +x ./gradlew -./gradlew assembleRelease -./gradlew bundleRelease + +if [[ "$TARGET" == "googleplay" ]]; then + ./gradlew assembleGooglePlayRelease + ./gradlew bundleGooglePlayRelease +else + ./gradlew assembleVanillaRelease + ./gradlew bundleVanillaRelease +fi ccache -s -v diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 918db8532..10b8ace96 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -221,51 +221,64 @@ jobs: android: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: ["vanilla", "googleplay"] env: CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_COMPILERCHECK: content CCACHE_SLOPPINESS: time_macros OS: android - TARGET: universal + TARGET: ${{ matrix.target }} + SHOULD_RUN: ${{ (matrix.target == 'vanilla' || github.ref_type == 'tag') }} steps: - uses: actions/checkout@v4 + if: ${{ env.SHOULD_RUN == 'true' }} with: submodules: recursive - name: Set up cache + if: ${{ env.SHOULD_RUN == 'true' }} uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper ${{ env.CCACHE_DIR }} - key: ${{ runner.os }}-android-${{ github.sha }} + key: ${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-android- + ${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}- - name: Set tag name + if: ${{ env.SHOULD_RUN == 'true' }} run: | if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV fi echo $GIT_TAG_NAME - - name: Deps + - name: Install tools + if: ${{ env.SHOULD_RUN == 'true' }} run: | sudo apt-get update -y sudo apt-get install ccache apksigner -y - name: Update Android SDK CMake version + if: ${{ env.SHOULD_RUN == 'true' }} run: | echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3" - name: Build + if: ${{ env.SHOULD_RUN == 'true' }} run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh env: ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }} - name: Pack + if: ${{ env.SHOULD_RUN == 'true' }} run: ../../../.ci/pack.sh working-directory: src/android/app env: UNPACKED: 1 - name: Upload + if: ${{ env.SHOULD_RUN == 'true' }} uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} diff --git a/externals/boost b/externals/boost index 04f8fd64d..2c82bd787 160000 --- a/externals/boost +++ b/externals/boost @@ -1 +1 @@ -Subproject commit 04f8fd64dababe6c82151792a134e3a03b395fe6 +Subproject commit 2c82bd787302398bcae990e3c9ab2b451284f4ca diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index c030c04ca..0973a34c7 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -164,6 +164,18 @@ android { flavorDimensions.add("version") + productFlavors { + register("vanilla") { + isDefault = true + dimension = "version" + versionNameSuffix = "-vanilla" + } + register("googlePlay") { + dimension = "version" + versionNameSuffix = "-googleplay" + } + } + externalNativeBuild { cmake { version = "3.25.0+" diff --git a/src/android/app/src/googlePlay/AndroidManifest.xml b/src/android/app/src/googlePlay/AndroidManifest.xml new file mode 100644 index 000000000..a95b9539c --- /dev/null +++ b/src/android/app/src/googlePlay/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + + 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 75c97b101..bce4ac490 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 @@ -679,6 +679,10 @@ object NativeLibrary { FileUtil.getFileSize(path) } + @Keep + @JvmStatic + fun getBuildFlavor(): String = BuildConfig.FLAVOR + @Keep @JvmStatic fun fileExists(path: String): Boolean = @@ -717,11 +721,37 @@ object NativeLibrary { ) } + @Keep + @JvmStatic + fun renameFile(path: String, destinationFilename: String): Boolean = + if (FileUtil.isNativePath(path)) { + try { + CitraApplication.documentsTree.renameFile(path, destinationFilename) + } catch (e: Exception) { + false + } + } else { + FileUtil.renameFile(path, destinationFilename) + } + @Keep @JvmStatic fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean = CitraApplication.documentsTree.updateDocumentLocation(sourcePath, destinationPath) + @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/fragments/GrantMissingFilesystemPermissionFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/GrantMissingFilesystemPermissionFragment.kt index 449b98fc0..e07787c52 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/GrantMissingFilesystemPermissionFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/GrantMissingFilesystemPermissionFragment.kt @@ -19,10 +19,13 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.citra.citra_emu.R import org.citra.citra_emu.ui.main.MainActivity +import org.citra.citra_emu.utils.BuildUtil + class GrantMissingFilesystemPermissionFragment : DialogFragment() { private lateinit var mainActivity: MainActivity override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + BuildUtil.assertNotGooglePlay() mainActivity = requireActivity() as MainActivity isCancelable = false @@ -71,6 +74,7 @@ class GrantMissingFilesystemPermissionFragment : DialogFragment() { const val TAG = "GrantMissingFilesystemPermissionFragment" fun newInstance(): GrantMissingFilesystemPermissionFragment { + BuildUtil.assertNotGooglePlay() return GrantMissingFilesystemPermissionFragment() } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt index 61834e444..4cc141235 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt @@ -32,6 +32,7 @@ import androidx.preference.PreferenceManager import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.snackbar.Snackbar import com.google.android.material.transition.MaterialFadeThrough +import org.citra.citra_emu.BuildConfig import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R @@ -44,6 +45,7 @@ import org.citra.citra_emu.model.PageState import org.citra.citra_emu.model.SetupCallback import org.citra.citra_emu.model.SetupPage import org.citra.citra_emu.ui.main.MainActivity +import org.citra.citra_emu.utils.BuildUtil import org.citra.citra_emu.utils.CitraDirectoryHelper import org.citra.citra_emu.utils.GameHelper import org.citra.citra_emu.utils.PermissionsHandler @@ -145,53 +147,56 @@ class SetupFragment : Fragment() { false, 0, pageButtons = mutableListOf().apply { - add( - PageButton( - R.drawable.ic_folder, - R.string.filesystem_permission, - R.string.filesystem_permission_description, - buttonAction = { - pageButtonCallback = it - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - manageExternalStoragePermissionLauncher.launch( - Intent( - android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, - Uri.fromParts( - "package", - requireActivity().packageName, - null + @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") + if (BuildConfig.FLAVOR != "googlePlay") { + add( + PageButton( + R.drawable.ic_folder, + R.string.filesystem_permission, + R.string.filesystem_permission_description, + buttonAction = { + pageButtonCallback = it + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + manageExternalStoragePermissionLauncher.launch( + Intent( + android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + Uri.fromParts( + "package", + requireActivity().packageName, + null + ) ) ) - ) - } else { - permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } - }, - buttonState = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (Environment.isExternalStorageManager()) { - ButtonState.BUTTON_ACTION_COMPLETE } else { - ButtonState.BUTTON_ACTION_INCOMPLETE + permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } - } else { - if (ContextCompat.checkSelfPermission( - requireContext(), - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED - ) { - ButtonState.BUTTON_ACTION_COMPLETE + }, + buttonState = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (Environment.isExternalStorageManager()) { + ButtonState.BUTTON_ACTION_COMPLETE + } else { + ButtonState.BUTTON_ACTION_INCOMPLETE + } } else { - ButtonState.BUTTON_ACTION_INCOMPLETE + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + ) { + ButtonState.BUTTON_ACTION_COMPLETE + } else { + ButtonState.BUTTON_ACTION_INCOMPLETE + } } - } - }, - isUnskippable = true, - hasWarning = true, - R.string.filesystem_permission_warning, - R.string.filesystem_permission_warning_description, + }, + isUnskippable = true, + hasWarning = true, + R.string.filesystem_permission_warning, + R.string.filesystem_permission_warning_description, + ) ) - ) + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { add( PageButton( @@ -279,13 +284,18 @@ class SetupFragment : Fragment() { NotificationManagerCompat.from(requireContext()) .areNotificationsEnabled() // External Storage - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - permissionsComplete = (permissionsComplete && Environment.isExternalStorageManager()) - } else { - permissionsComplete = (permissionsComplete && ContextCompat.checkSelfPermission( - requireContext(), - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED) + @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") + if (BuildConfig.FLAVOR != "googlePlay") { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + permissionsComplete = + (permissionsComplete && Environment.isExternalStorageManager()) + } else { + permissionsComplete = + (permissionsComplete && ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED) + } } if (permissionsComplete) { @@ -542,6 +552,7 @@ class SetupFragment : Fragment() { @RequiresApi(Build.VERSION_CODES.R) private val manageExternalStoragePermissionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + BuildUtil.assertNotGooglePlay() if (Environment.isExternalStorageManager()) { checkForButtonState.invoke() return@registerForActivityResult diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt index dfe7c4272..3a789b8b5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt @@ -40,6 +40,7 @@ import androidx.work.WorkManager import com.google.android.material.color.MaterialColors import com.google.android.material.navigation.NavigationBarView import kotlinx.coroutines.launch +import org.citra.citra_emu.BuildConfig import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.contracts.OpenFileResultContract @@ -198,21 +199,25 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return } - fun requestMissingFilesystemPermission() = - GrantMissingFilesystemPermissionFragment.newInstance() - .show(supportFragmentManager,GrantMissingFilesystemPermissionFragment.TAG) + @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") + if (BuildConfig.FLAVOR != "googlePlay") { + fun requestMissingFilesystemPermission() = + GrantMissingFilesystemPermissionFragment.newInstance() + .show(supportFragmentManager, GrantMissingFilesystemPermissionFragment.TAG) - if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (!Environment.isExternalStorageManager()) { - requestMissingFilesystemPermission() - } - } else { - if (ContextCompat.checkSelfPermission( - this, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) != PackageManager.PERMISSION_GRANTED) { - requestMissingFilesystemPermission() + if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + requestMissingFilesystemPermission() + } + } else { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + requestMissingFilesystemPermission() + } } } } @@ -231,10 +236,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return } - if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) { - if (NativeLibrary.getUserDirectory() == "") { - SelectUserDirectoryDialogFragment.newInstance(this) - .show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG) + @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") + if (BuildConfig.FLAVOR != "googlePlay") { + if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) { + if (NativeLibrary.getUserDirectory() == "") { + SelectUserDirectoryDialogFragment.newInstance(this) + .show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG) + } } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/BuildUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/BuildUtil.kt new file mode 100644 index 000000000..028c6d516 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/BuildUtil.kt @@ -0,0 +1,17 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.utils + +import org.citra.citra_emu.BuildConfig + +object BuildUtil { + fun assertNotGooglePlay() { + + @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") + if (BuildConfig.FLAVOR == "googlePlay") { + error("Non-GooglePlay code being called in GooglePlay build") + } + } +} 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 d7ac6f76a..d5c4f791d 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 @@ -192,6 +192,33 @@ class DocumentsTree { } } + @Synchronized + fun renameFile(filepath: String, destinationFilename: String): Boolean { + val node = resolvePath(filepath) ?: return false + try { + val filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD) + val newUri = DocumentsContract.renameDocument(context.contentResolver, node.uri!!, filename) + node.rename(filename, newUri) + return true + } catch (e: Exception) { + error("[DocumentsTree]: Cannot rename file, error: " + e.message) + } + } + + @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!!) + updateDocumentLocation("$sourceDirPath/$filename", "$destDirPath/$filename") + 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 @@ -210,15 +237,15 @@ class DocumentsTree { @Synchronized fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean { - Log.error("Got paths: $sourcePath, $destinationPath") val sourceNode = resolvePath(sourcePath) val newName = Paths.get(destinationPath).fileName.toString() val parentPath = Paths.get(destinationPath).parent.toString() val newParent = resolvePath(parentPath) val newUri = (getUri(parentPath).toString() + "%2F$newName").toUri() // <- Is there a better way? - if (sourceNode == null || newParent == null) + if (sourceNode == null || newParent == null) { return false + } sourceNode.parent!!.removeChild(sourceNode) @@ -300,6 +327,15 @@ class DocumentsTree { loaded = true } + @Synchronized + fun rename(name: String, uri: Uri?) { + parent ?: return + parent!!.removeChild(this) + this.name = name + this.uri = uri + parent!!.addChild(this) + } + fun addChild(node: DocumentsNode) { children[node.name.lowercase()] = node } 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 61fcf9bfa..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 @@ -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 @@ -422,6 +423,32 @@ object FileUtil { } } + @JvmStatic + fun renameFile(path: String, destinationFilename: String): Boolean { + try { + val uri = Uri.parse(path) + DocumentsContract.renameDocument(context.contentResolver, uri, destinationFilename) + return true + } catch (e: Exception) { + Log.error("[FileUtil]: Cannot rename file, error: " + e.message) + } + 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/android/app/src/main/java/org/citra/citra_emu/utils/RemovableStorageHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/RemovableStorageHelper.kt index 1a859242d..80e1d8944 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/RemovableStorageHelper.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/RemovableStorageHelper.kt @@ -4,6 +4,7 @@ package org.citra.citra_emu.utils +import org.citra.citra_emu.utils.BuildUtil import java.io.File object RemovableStorageHelper { @@ -12,6 +13,8 @@ object RemovableStorageHelper { // Apparently, on certain devices the mount location can vary, so add // extra cases here if we discover any new ones. fun getRemovableStoragePath(idString: String): String? { + BuildUtil.assertNotGooglePlay() + var pathFile: File pathFile = File("/mnt/media_rw/$idString"); diff --git a/src/common/android_storage.cpp b/src/common/android_storage.cpp index 542c1b172..0dfd98c25 100644 --- a/src/common/android_storage.cpp +++ b/src/common/android_storage.cpp @@ -3,7 +3,11 @@ // Refer to the license.txt file included. #ifdef ANDROID +#include +#include #include "common/android_storage.h" +#include "common/file_util.h" +#include "common/logging/log.h" namespace AndroidStorage { JNIEnv* GetEnvForThread() { @@ -80,8 +84,9 @@ void CleanupJNI() { } bool CreateFile(const std::string& directory, const std::string& filename) { - if (create_file == nullptr) + if (create_file == nullptr) { return false; + } auto env = GetEnvForThread(); jstring j_directory = env->NewStringUTF(directory.c_str()); jstring j_filename = env->NewStringUTF(filename.c_str()); @@ -89,8 +94,9 @@ bool CreateFile(const std::string& directory, const std::string& filename) { } bool CreateDir(const std::string& directory, const std::string& filename) { - if (create_dir == nullptr) + if (create_dir == nullptr) { return false; + } auto env = GetEnvForThread(); jstring j_directory = env->NewStringUTF(directory.c_str()); jstring j_directory_name = env->NewStringUTF(filename.c_str()); @@ -98,8 +104,9 @@ bool CreateDir(const std::string& directory, const std::string& filename) { } int OpenContentUri(const std::string& filepath, AndroidOpenMode openmode) { - if (open_content_uri == nullptr) + if (open_content_uri == nullptr) { return -1; + } const char* mode = ""; switch (openmode) { @@ -135,8 +142,9 @@ int OpenContentUri(const std::string& filepath, AndroidOpenMode openmode) { std::vector GetFilesName(const std::string& filepath) { auto vector = std::vector(); - if (get_files_name == nullptr) + if (get_files_name == nullptr) { return vector; + } auto env = GetEnvForThread(); jstring j_filepath = env->NewStringUTF(filepath.c_str()); auto j_object = @@ -151,9 +159,10 @@ std::vector GetFilesName(const std::string& filepath) { } std::optional GetUserDirectory() { - if (get_user_directory == nullptr) + if (get_user_directory == nullptr) { throw std::runtime_error( "Unable to locate user directory: Function with ID 'get_user_directory' is missing"); + } auto env = GetEnvForThread(); auto j_user_directory = (jstring)(env->CallStaticObjectMethod(native_library, get_user_directory, nullptr)); @@ -164,10 +173,22 @@ std::optional GetUserDirectory() { return result; } +std::string GetBuildFlavor() { + if (get_build_flavor == nullptr) { + throw std::runtime_error( + "Unable get build flavor: Function with ID 'get_build_flavor' is missing"); + } + auto env = GetEnvForThread(); + const auto jflavor = + (jstring)(env->CallStaticObjectMethod(native_library, get_build_flavor, nullptr)); + return env->GetStringUTFChars(jflavor, nullptr); +} + bool CopyFile(const std::string& source, const std::string& destination_path, const std::string& destination_filename) { - if (copy_file == nullptr) + if (copy_file == nullptr) { return false; + } auto env = GetEnvForThread(); jstring j_source_path = env->NewStringUTF(source.c_str()); jstring j_destination_path = env->NewStringUTF(destination_path.c_str()); @@ -176,9 +197,26 @@ bool CopyFile(const std::string& source, const std::string& destination_path, j_destination_path, j_destination_filename); } -bool UpdateDocumentLocation(const std::string& source_path, const std::string& destination_path) { - if (update_document_location == nullptr) +bool RenameFile(const std::string& source, const std::string& filename) { + if (rename_file == nullptr) { return false; + } + if (std::string(FileUtil::GetFilename(source)) == + std::string(FileUtil::GetFilename(filename))) { + // TODO: Should this be treated as a success or failure? + return false; + } + auto env = GetEnvForThread(); + jstring j_source_path = env->NewStringUTF(source.c_str()); + jstring j_destination_path = env->NewStringUTF(filename.c_str()); + return env->CallStaticBooleanMethod(native_library, rename_file, j_source_path, + j_destination_path); +} + +bool UpdateDocumentLocation(const std::string& source_path, const std::string& destination_path) { + if (update_document_location == nullptr) { + return false; + } auto env = GetEnvForThread(); jstring j_source_path = env->NewStringUTF(source_path.c_str()); jstring j_destination_path = env->NewStringUTF(destination_path.c_str()); @@ -186,6 +224,82 @@ bool UpdateDocumentLocation(const std::string& source_path, const std::string& d 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; + } + if (source_dir_path == destination_dir_path) { + // TODO: Should this be treated as a success or failure? + 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) { + if (src_full_path == dest_full_path) { + // TODO: Should this be treated as a success or failure? + return false; + } + 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; + + const std::string tmp_path = "/tmp"; + AndroidStorage::CreateDir("/", "tmp"); + + // If a simultaneous move and rename are not necessary, use individual methods + // TODO: Uncomment this code for 2123.4 RC to allow stress testing of move + rename process in + // beta + /* + if (src_filename == dest_filename || src_parent_path == dest_parent_path) { + if (src_filename != dest_filename) { + return AndroidStorage::RenameFile(src_full_path, dest_filename); + } else if (src_parent_path != dest_parent_path) { + return AndroidStorage::MoveFile(src_filename, src_parent_path, dest_parent_path); + } + } + */ + + // Step 1: Create directory named after UUID inside /tmp to house the moved file. + // This prevents clashes if files with the same name are moved simultaneously. + const auto uuid = boost::uuids::to_string(boost::uuids::time_generator_v7()()); + const auto allocated_tmp_path = tmp_path + "/" + uuid; + AndroidStorage::CreateDir(tmp_path, uuid); + + // Step 2: Attempt to move to allocated temporary directory. + // If this step fails, skip everything except the cleanup. + result = AndroidStorage::MoveFile(src_filename, src_parent_path, allocated_tmp_path); + if (result == true) { + // Step 3: Rename to desired file name. + if (src_filename != dest_filename) { // TODO: Remove this if statement in 2123.4 RC, keeping + // the RenameFile call + AndroidStorage::RenameFile((allocated_tmp_path + "/" + src_filename), dest_filename); + } + + // Step 4: If a file with the desired name in the destination exists, remove it. + AndroidStorage::DeleteDocument(dest_full_path); + + // Step 5: Attempt to move file to desired location. + // If this step fails, move the file back to where it came from. + result = AndroidStorage::MoveFile(dest_filename, allocated_tmp_path, dest_parent_path); + if (result == false) { + AndroidStorage::MoveAndRenameFile((allocated_tmp_path + "/" + dest_filename), + src_full_path); + } + } + // Step 6: Clean up the allocated temp directory. + AndroidStorage::DeleteDocument(allocated_tmp_path); + 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 aca474595..a0e54f77c 100644 --- a/src/common/android_storage.h +++ b/src/common/android_storage.h @@ -25,10 +25,17 @@ (const std::string& source, const std::string& destination_path, \ 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") \ V(UpdateDocumentLocation, bool, \ (const std::string& source_path, const std::string& destination_path), \ update_document_location, "updateDocumentLocation", \ - "(Ljava/lang/String;Ljava/lang/String;)Z") + "(Ljava/lang/String;Ljava/lang/String;)Z") \ + V(GetBuildFlavor, std::string, (), get_build_flavor, "getBuildFlavor", "()Ljava/lang/String;") \ + 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") \ @@ -48,6 +55,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 73a298965..49589d6f1 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -304,25 +304,31 @@ 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, "{} --> {}", srcPath, 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 - std::optional userDirLocation = AndroidStorage::GetUserDirectory(); - if (userDirLocation && rename((*userDirLocation + srcFilename).c_str(), - (*userDirLocation + destFilename).c_str()) == 0) { - AndroidStorage::UpdateDocumentLocation(srcFilename, destFilename); - // ^ TODO: This shouldn't fail, but what should we do if it somehow does? - return true; + // srcFullPath and destFullPath are relative to the user directory + if (AndroidStorage::GetBuildFlavor() == "googlePlay") { + if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath)) + return true; + } else { + std::optional userDirLocation = AndroidStorage::GetUserDirectory(); + if (userDirLocation && rename((*userDirLocation + srcFullPath).c_str(), + (*userDirLocation + destFullPath).c_str()) == 0) { + AndroidStorage::UpdateDocumentLocation(srcFullPath, destFullPath); + // ^ TODO: This shouldn't fail, but what should we do if it somehow does? + 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 6910f7223..4c7d21349 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -135,13 +135,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); /**