android: Implemented googlePlay build variants

This commit is contained in:
OpenSauce04 2025-12-28 23:56:26 +00:00
parent f911af4197
commit 33e7ed5c5c
13 changed files with 214 additions and 72 deletions

View File

@ -164,6 +164,18 @@ android {
flavorDimensions.add("version") flavorDimensions.add("version")
productFlavors {
register("vanilla") {
isDefault = true
dimension = "version"
versionNameSuffix = "-vanilla"
}
register("googlePlay") {
dimension = "version"
versionNameSuffix = "-googleplay"
}
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
version = "3.25.0+" version = "3.25.0+"

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- These permissions aren't allowed by Google. We asked, and they declined. -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:node="remove" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
</manifest>

View File

@ -673,6 +673,10 @@ object NativeLibrary {
FileUtil.getFileSize(path) FileUtil.getFileSize(path)
} }
@Keep
@JvmStatic
fun getBuildFlavor(): String = BuildConfig.FLAVOR
@Keep @Keep
@JvmStatic @JvmStatic
fun fileExists(path: String): Boolean = 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 @Keep
@JvmStatic @JvmStatic
fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean = fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean =

View File

@ -19,10 +19,13 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.R import org.citra.citra_emu.R
import org.citra.citra_emu.ui.main.MainActivity import org.citra.citra_emu.ui.main.MainActivity
import org.citra.citra_emu.utils.BuildUtil
class GrantMissingFilesystemPermissionFragment : DialogFragment() { class GrantMissingFilesystemPermissionFragment : DialogFragment() {
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
BuildUtil.assertNotGooglePlay()
mainActivity = requireActivity() as MainActivity mainActivity = requireActivity() as MainActivity
isCancelable = false isCancelable = false
@ -71,6 +74,7 @@ class GrantMissingFilesystemPermissionFragment : DialogFragment() {
const val TAG = "GrantMissingFilesystemPermissionFragment" const val TAG = "GrantMissingFilesystemPermissionFragment"
fun newInstance(): GrantMissingFilesystemPermissionFragment { fun newInstance(): GrantMissingFilesystemPermissionFragment {
BuildUtil.assertNotGooglePlay()
return GrantMissingFilesystemPermissionFragment() return GrantMissingFilesystemPermissionFragment()
} }
} }

View File

@ -32,6 +32,7 @@ import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import org.citra.citra_emu.BuildConfig
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R 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.SetupCallback
import org.citra.citra_emu.model.SetupPage import org.citra.citra_emu.model.SetupPage
import org.citra.citra_emu.ui.main.MainActivity 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.CitraDirectoryHelper
import org.citra.citra_emu.utils.GameHelper import org.citra.citra_emu.utils.GameHelper
import org.citra.citra_emu.utils.PermissionsHandler import org.citra.citra_emu.utils.PermissionsHandler
@ -145,53 +147,56 @@ class SetupFragment : Fragment() {
false, false,
0, 0,
pageButtons = mutableListOf<PageButton>().apply { pageButtons = mutableListOf<PageButton>().apply {
add( @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
PageButton( if (BuildConfig.FLAVOR != "googlePlay") {
R.drawable.ic_folder, add(
R.string.filesystem_permission, PageButton(
R.string.filesystem_permission_description, R.drawable.ic_folder,
buttonAction = { R.string.filesystem_permission,
pageButtonCallback = it R.string.filesystem_permission_description,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { buttonAction = {
manageExternalStoragePermissionLauncher.launch( pageButtonCallback = it
Intent( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, manageExternalStoragePermissionLauncher.launch(
Uri.fromParts( Intent(
"package", android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
requireActivity().packageName, Uri.fromParts(
null "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 { } else {
ButtonState.BUTTON_ACTION_INCOMPLETE permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} }
} else { },
if (ContextCompat.checkSelfPermission( buttonState = {
requireContext(), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Manifest.permission.WRITE_EXTERNAL_STORAGE if (Environment.isExternalStorageManager()) {
) == PackageManager.PERMISSION_GRANTED ButtonState.BUTTON_ACTION_COMPLETE
) { } else {
ButtonState.BUTTON_ACTION_COMPLETE ButtonState.BUTTON_ACTION_INCOMPLETE
}
} else { } 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,
isUnskippable = true, hasWarning = true,
hasWarning = true, R.string.filesystem_permission_warning,
R.string.filesystem_permission_warning, R.string.filesystem_permission_warning_description,
R.string.filesystem_permission_warning_description, )
) )
) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
add( add(
PageButton( PageButton(
@ -279,13 +284,18 @@ class SetupFragment : Fragment() {
NotificationManagerCompat.from(requireContext()) NotificationManagerCompat.from(requireContext())
.areNotificationsEnabled() .areNotificationsEnabled()
// External Storage // External Storage
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
permissionsComplete = (permissionsComplete && Environment.isExternalStorageManager()) if (BuildConfig.FLAVOR != "googlePlay") {
} else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
permissionsComplete = (permissionsComplete && ContextCompat.checkSelfPermission( permissionsComplete =
requireContext(), (permissionsComplete && Environment.isExternalStorageManager())
Manifest.permission.WRITE_EXTERNAL_STORAGE } else {
) == PackageManager.PERMISSION_GRANTED) permissionsComplete =
(permissionsComplete && ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED)
}
} }
if (permissionsComplete) { if (permissionsComplete) {
@ -542,6 +552,7 @@ class SetupFragment : Fragment() {
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private val manageExternalStoragePermissionLauncher = private val manageExternalStoragePermissionLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
BuildUtil.assertNotGooglePlay()
if (Environment.isExternalStorageManager()) { if (Environment.isExternalStorageManager()) {
checkForButtonState.invoke() checkForButtonState.invoke()
return@registerForActivityResult return@registerForActivityResult

View File

@ -40,6 +40,7 @@ import androidx.work.WorkManager
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.citra.citra_emu.BuildConfig
import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R import org.citra.citra_emu.R
import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.contracts.OpenFileResultContract
@ -195,21 +196,25 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return return
} }
fun requestMissingFilesystemPermission() = @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
GrantMissingFilesystemPermissionFragment.newInstance() if (BuildConfig.FLAVOR != "googlePlay") {
.show(supportFragmentManager,GrantMissingFilesystemPermissionFragment.TAG) fun requestMissingFilesystemPermission() =
GrantMissingFilesystemPermissionFragment.newInstance()
.show(supportFragmentManager, GrantMissingFilesystemPermissionFragment.TAG)
if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) { if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) { if (!Environment.isExternalStorageManager()) {
requestMissingFilesystemPermission() requestMissingFilesystemPermission()
} }
} else { } else {
if (ContextCompat.checkSelfPermission( if (ContextCompat.checkSelfPermission(
this, this,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED) { ) != PackageManager.PERMISSION_GRANTED
requestMissingFilesystemPermission() ) {
requestMissingFilesystemPermission()
}
} }
} }
} }
@ -228,10 +233,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return return
} }
if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) { @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
if (NativeLibrary.getUserDirectory() == "") { if (BuildConfig.FLAVOR != "googlePlay") {
SelectUserDirectoryDialogFragment.newInstance(this) if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) {
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG) if (NativeLibrary.getUserDirectory() == "") {
SelectUserDirectoryDialogFragment.newInstance(this)
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
}
} }
} }
} }

View File

@ -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")
}
}
}

View File

@ -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 @Synchronized
fun deleteDocument(filepath: String): Boolean { fun deleteDocument(filepath: String): Boolean {
val node = resolvePath(filepath) ?: return false val node = resolvePath(filepath) ?: return false
@ -300,6 +313,15 @@ class DocumentsTree {
loaded = true 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) { fun addChild(node: DocumentsNode) {
children[node.name.lowercase()] = node children[node.name.lowercase()] = node
} }

View File

@ -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 @JvmStatic
fun deleteDocument(path: String): Boolean { fun deleteDocument(path: String): Boolean {
try { try {

View File

@ -4,6 +4,7 @@
package org.citra.citra_emu.utils package org.citra.citra_emu.utils
import org.citra.citra_emu.utils.BuildUtil
import java.io.File import java.io.File
object RemovableStorageHelper { object RemovableStorageHelper {
@ -12,6 +13,8 @@ object RemovableStorageHelper {
// Apparently, on certain devices the mount location can vary, so add // Apparently, on certain devices the mount location can vary, so add
// extra cases here if we discover any new ones. // extra cases here if we discover any new ones.
fun getRemovableStoragePath(idString: String): String? { fun getRemovableStoragePath(idString: String): String? {
BuildUtil.assertNotGooglePlay()
var pathFile: File var pathFile: File
pathFile = File("/mnt/media_rw/$idString"); pathFile = File("/mnt/media_rw/$idString");

View File

@ -164,6 +164,16 @@ std::optional<std::string> GetUserDirectory() {
return result; 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, bool CopyFile(const std::string& source, const std::string& destination_path,
const std::string& destination_filename) { const std::string& destination_filename) {
if (copy_file == nullptr) 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); 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) { bool UpdateDocumentLocation(const std::string& source_path, const std::string& destination_path) {
if (update_document_location == nullptr) if (update_document_location == nullptr)
return false; return false;

View File

@ -25,10 +25,13 @@
(const std::string& source, const std::string& destination_path, \ (const std::string& source, const std::string& destination_path, \
const std::string& destination_filename), \ const std::string& destination_filename), \
copy_file, "copyFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z") \ 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, \ V(UpdateDocumentLocation, bool, \
(const std::string& source_path, const std::string& destination_path), \ (const std::string& source_path, const std::string& destination_path), \
update_document_location, "updateDocumentLocation", \ 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) \ #define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \
V(IsDirectory, bool, is_directory, CallStaticBooleanMethod, "isDirectory", \ V(IsDirectory, bool, is_directory, CallStaticBooleanMethod, "isDirectory", \
"(Ljava/lang/String;)Z") \ "(Ljava/lang/String;)Z") \

View File

@ -311,12 +311,17 @@ bool Rename(const std::string& srcFilename, const std::string& destFilename) {
Common::UTF8ToUTF16W(destFilename).c_str()) == 0) Common::UTF8ToUTF16W(destFilename).c_str()) == 0)
return true; return true;
#elif ANDROID #elif ANDROID
std::optional<std::string> userDirLocation = AndroidStorage::GetUserDirectory(); if (AndroidStorage::GetBuildFlavor() == "googlePlay") {
if (userDirLocation && rename((*userDirLocation + srcFilename).c_str(), if (AndroidStorage::RenameFile(srcFilename, std::string(GetFilename(destFilename))))
(*userDirLocation + destFilename).c_str()) == 0) { return true;
AndroidStorage::UpdateDocumentLocation(srcFilename, destFilename); } else {
// ^ TODO: This shouldn't fail, but what should we do if it somehow does? std::optional<std::string> userDirLocation = AndroidStorage::GetUserDirectory();
return true; 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 #else
if (rename(srcFilename.c_str(), destFilename.c_str()) == 0) if (rename(srcFilename.c_str(), destFilename.c_str()) == 0)