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);
/**