From 33e7ed5c5cb73f818ec390efa8f409571c9af683 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Sun, 28 Dec 2025 23:56:26 +0000 Subject: [PATCH] android: Implemented `googlePlay` build variants --- src/android/app/build.gradle.kts | 12 ++ .../app/src/googlePlay/AndroidManifest.xml | 8 ++ .../java/org/citra/citra_emu/NativeLibrary.kt | 17 +++ ...rantMissingFilesystemPermissionFragment.kt | 4 + .../citra_emu/fragments/SetupFragment.kt | 105 ++++++++++-------- .../citra/citra_emu/ui/main/MainActivity.kt | 44 +++++--- .../org/citra/citra_emu/utils/BuildUtil.kt | 17 +++ .../citra/citra_emu/utils/DocumentsTree.kt | 22 ++++ .../org/citra/citra_emu/utils/FileUtil.kt | 12 ++ .../citra_emu/utils/RemovableStorageHelper.kt | 3 + src/common/android_storage.cpp | 20 ++++ src/common/android_storage.h | 5 +- src/common/file_util.cpp | 17 ++- 13 files changed, 214 insertions(+), 72 deletions(-) create mode 100644 src/android/app/src/googlePlay/AndroidManifest.xml create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/BuildUtil.kt 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)