diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index 7dc978920..46b8d500c 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 c6c97e762..7f44c3b0e 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
@@ -673,6 +673,10 @@ object NativeLibrary {
FileUtil.getFileSize(path)
}
+ @Keep
+ @JvmStatic
+ fun getBuildFlavor(): String = BuildConfig.FLAVOR
+
@Keep
@JvmStatic
fun fileExists(path: String): Boolean =
@@ -711,6 +715,19 @@ 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 =
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 4c8bb04c8..aaff56183 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
@@ -195,21 +196,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()
+ }
}
}
}
@@ -228,10 +233,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..8bb46027e 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,19 @@ 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 deleteDocument(filepath: String): Boolean {
val node = resolvePath(filepath) ?: return false
@@ -300,6 +313,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..e5bf4fea5 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
@@ -422,6 +422,18 @@ 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 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..8ce8b0084 100644
--- a/src/common/android_storage.cpp
+++ b/src/common/android_storage.cpp
@@ -164,6 +164,16 @@ 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)
@@ -176,6 +186,16 @@ bool CopyFile(const std::string& source, const std::string& destination_path,
j_destination_path, j_destination_filename);
}
+bool RenameFile(const std::string& source, const std::string& filename) {
+ if (rename_file == nullptr)
+ 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;
diff --git a/src/common/android_storage.h b/src/common/android_storage.h
index aca474595..1c23ee06c 100644
--- a/src/common/android_storage.h
+++ b/src/common/android_storage.h
@@ -25,10 +25,13 @@
(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;")
#define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \
V(IsDirectory, bool, is_directory, CallStaticBooleanMethod, "isDirectory", \
"(Ljava/lang/String;)Z") \
diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp
index 794e2080c..52418bd2a 100644
--- a/src/common/file_util.cpp
+++ b/src/common/file_util.cpp
@@ -311,12 +311,17 @@ bool Rename(const std::string& srcFilename, const std::string& destFilename) {
Common::UTF8ToUTF16W(destFilename).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;
+ if (AndroidStorage::GetBuildFlavor() == "googlePlay") {
+ if (AndroidStorage::RenameFile(srcFilename, std::string(GetFilename(destFilename))))
+ return true;
+ } else {
+ 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;
+ }
}
#else
if (rename(srcFilename.c_str(), destFilename.c_str()) == 0)