diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index a7e3581ee..abc14939b 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -29,6 +29,8 @@
+
+
if (!PermissionsHandler.hasWriteAccess(requireContext())) {
- (requireActivity() as MainActivity)?.openCitraDirectory?.launch(null)
+ PermissionsHandler.compatibleSelectDirectory((requireActivity() as MainActivity).openCitraDirectory)
}
}
.show()
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
new file mode 100644
index 000000000..449b98fc0
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/GrantMissingFilesystemPermissionFragment.kt
@@ -0,0 +1,77 @@
+// 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.fragments
+
+import android.Manifest
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.provider.Settings
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+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
+class GrantMissingFilesystemPermissionFragment : DialogFragment() {
+ private lateinit var mainActivity: MainActivity
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ mainActivity = requireActivity() as MainActivity
+
+ isCancelable = false
+
+ val requestPermissionFunction =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ {
+ manageExternalStoragePermissionLauncher.launch(
+ Intent(
+ Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
+ Uri.fromParts("package", mainActivity.packageName, null)
+ )
+ )
+ }
+ } else {
+ { permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) }
+ }
+
+
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.filesystem_permission_warning)
+ .setMessage(R.string.filesystem_permission_lost)
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+ requestPermissionFunction()
+ }
+ .show()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ private val manageExternalStoragePermissionLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (Environment.isExternalStorageManager()) {
+ return@registerForActivityResult
+ }
+ }
+
+ private val permissionLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ if (isGranted) {
+ return@registerForActivityResult
+ }
+ }
+
+ companion object {
+ const val TAG = "GrantMissingFilesystemPermissionFragment"
+
+ fun newInstance(): GrantMissingFilesystemPermissionFragment {
+ return GrantMissingFilesystemPermissionFragment()
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
index 432a06aa0..63c435531 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
@@ -159,7 +159,7 @@ class HomeSettingsFragment : Fragment() {
R.string.select_citra_user_folder,
R.string.select_citra_user_folder_home_description,
R.drawable.ic_home,
- { mainActivity?.openCitraDirectory?.launch(null) },
+ { PermissionsHandler.compatibleSelectDirectory(mainActivity.openCitraDirectory) },
details = homeViewModel.userDir
),
HomeSetting(
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SelectUserDirectoryDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SelectUserDirectoryDialogFragment.kt
index 988f90ccc..f0f945860 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SelectUserDirectoryDialogFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SelectUserDirectoryDialogFragment.kt
@@ -13,21 +13,25 @@ import androidx.lifecycle.ViewModelProvider
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.PermissionsHandler
import org.citra.citra_emu.viewmodel.HomeViewModel
-class SelectUserDirectoryDialogFragment : DialogFragment() {
+class SelectUserDirectoryDialogFragment(titleOverride: Int? = null, descriptionOverride: Int? = null) : DialogFragment() {
private lateinit var mainActivity: MainActivity
+ private val title = titleOverride ?: R.string.select_citra_user_folder
+ private val description = descriptionOverride ?: R.string.selecting_user_directory_without_write_permissions
+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
mainActivity = requireActivity() as MainActivity
isCancelable = false
return MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.select_citra_user_folder)
- .setMessage(R.string.selecting_user_directory_without_write_permissions)
+ .setTitle(title)
+ .setMessage(description)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
- mainActivity?.openCitraDirectoryLostPermission?.launch(null)
+ PermissionsHandler.compatibleSelectDirectory(mainActivity.openCitraDirectoryLostPermission)
}
.show()
}
@@ -35,9 +39,10 @@ class SelectUserDirectoryDialogFragment : DialogFragment() {
companion object {
const val TAG = "SelectUserDirectoryDialogFragment"
- fun newInstance(activity: FragmentActivity): SelectUserDirectoryDialogFragment {
+ fun newInstance(activity: FragmentActivity, titleOverride: Int? = null, descriptionOverride: Int? = null):
+ SelectUserDirectoryDialogFragment {
ViewModelProvider(activity)[HomeViewModel::class.java].setPickingUserDir(true)
- return SelectUserDirectoryDialogFragment()
+ return SelectUserDirectoryDialogFragment(titleOverride, descriptionOverride)
}
}
}
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 fa47f99b8..61834e444 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
@@ -11,11 +11,13 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@@ -31,6 +33,7 @@ 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.CitraApplication
+import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.adapters.SetupAdapter
import org.citra.citra_emu.databinding.FragmentSetupBinding
@@ -142,7 +145,54 @@ class SetupFragment : Fragment() {
false,
0,
pageButtons = mutableListOf().apply {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ 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
+ }
+ } else {
+ 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,
+ )
+ )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
add(
PageButton(
R.drawable.ic_notification,
@@ -214,18 +264,31 @@ class SetupFragment : Fragment() {
)
},
) {
- if (
+ var permissionsComplete =
+ // Microphone
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED &&
+ // Camera
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED &&
+ // Notifications
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)
+ }
+
+ if (permissionsComplete) {
PageState.PAGE_STEPS_COMPLETE
} else {
PageState.PAGE_STEPS_INCOMPLETE
@@ -249,7 +312,7 @@ class SetupFragment : Fragment() {
R.string.select_citra_user_folder_description,
buttonAction = {
pageButtonCallback = it
- openCitraDirectory.launch(null)
+ PermissionsHandler.compatibleSelectDirectory(openCitraDirectory)
},
buttonState = {
if (PermissionsHandler.hasWriteAccess(requireContext())) {
@@ -452,6 +515,19 @@ class SetupFragment : Fragment() {
}
}
+ private fun showPermissionDeniedSnackbar() {
+ Snackbar.make(binding.root, R.string.permission_denied, Snackbar.LENGTH_LONG)
+ .setAnchorView(binding.buttonNext)
+ .setAction(R.string.grid_menu_core_settings) {
+ val intent =
+ Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", requireActivity().packageName, null)
+ intent.data = uri
+ startActivity(intent)
+ }
+ .show()
+ }
+
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
@@ -459,16 +535,19 @@ class SetupFragment : Fragment() {
return@registerForActivityResult
}
- Snackbar.make(binding.root, R.string.permission_denied, Snackbar.LENGTH_LONG)
- .setAnchorView(binding.buttonNext)
- .setAction(R.string.grid_menu_core_settings) {
- val intent =
- Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri = Uri.fromParts("package", requireActivity().packageName, null)
- intent.data = uri
- startActivity(intent)
- }
- .show()
+ showPermissionDeniedSnackbar()
+ }
+
+ // We can't use permissionLauncher because MANAGE_EXTERNAL_STORAGE is a special snowflake
+ @RequiresApi(Build.VERSION_CODES.R)
+ private val manageExternalStoragePermissionLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (Environment.isExternalStorageManager()) {
+ checkForButtonState.invoke()
+ return@registerForActivityResult
+ }
+
+ showPermissionDeniedSnackbar()
}
private val openCitraDirectory = registerForActivityResult(
@@ -478,6 +557,15 @@ class SetupFragment : Fragment() {
return@registerForActivityResult
}
+ if (NativeLibrary.getUserDirectory(result) == "") {
+ SelectUserDirectoryDialogFragment.newInstance(
+ mainActivity,
+ R.string.invalid_selection,
+ R.string.invalid_user_directory
+ ).show(mainActivity.supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
+ return@registerForActivityResult
+ }
+
CitraDirectoryHelper(requireActivity(), true).showCitraDirectoryDialog(result, pageButtonCallback, checkForButtonState)
}
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 37bff3396..9b9f55ce7 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
@@ -4,9 +4,12 @@
package org.citra.citra_emu.ui.main
+import android.Manifest
import android.content.Intent
+import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
+import android.os.Environment
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
@@ -36,6 +39,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.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityMainBinding
@@ -43,6 +47,7 @@ import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
+import org.citra.citra_emu.fragments.GrantMissingFilesystemPermissionFragment
import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment
import org.citra.citra_emu.fragments.UpdateUserDirectoryDialogFragment
import org.citra.citra_emu.utils.CiaInstallWorker
@@ -185,14 +190,48 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
- if (!firstTimeSetup && !PermissionsHandler.hasWriteAccess(this) &&
- !homeViewModel.isPickingUserDir.value
- ) {
+ if (firstTimeSetup) {
+ return
+ }
+
+ 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 (homeViewModel.isPickingUserDir.value) {
+ return
+ }
+
+ if (!PermissionsHandler.hasWriteAccess(this)) {
SelectUserDirectoryDialogFragment.newInstance(this)
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
- } else if (!firstTimeSetup && !homeViewModel.isPickingUserDir.value && CitraDirectoryUtils.needToUpdateManually()) {
+ return
+ } else if (CitraDirectoryUtils.needToUpdateManually()) {
UpdateUserDirectoryDialogFragment.newInstance(this)
.show(supportFragmentManager,UpdateUserDirectoryDialogFragment.TAG)
+ return
+ }
+
+ if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) {
+ if (NativeLibrary.getUserDirectory() == "") {
+ SelectUserDirectoryDialogFragment.newInstance(this)
+ .show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
+ }
}
}
@@ -316,6 +355,15 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult
}
+ if (NativeLibrary.getUserDirectory(result) == "") {
+ SelectUserDirectoryDialogFragment.newInstance(
+ this,
+ R.string.invalid_selection,
+ R.string.invalid_user_directory
+ ).show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
+ return@registerForActivityResult
+ }
+
CitraDirectoryHelper(this@MainActivity, permissionsLost)
.showCitraDirectoryDialog(result, buttonState = {})
}
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 e2b015d47..d7ac6f76a 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
@@ -6,10 +6,12 @@ package org.citra.citra_emu.utils
import android.net.Uri
import android.provider.DocumentsContract
+import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.model.CheapDocument
import java.net.URLDecoder
+import java.nio.file.Paths
import java.util.StringTokenizer
import java.util.concurrent.ConcurrentHashMap
@@ -190,19 +192,6 @@ 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
@@ -219,6 +208,29 @@ 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)
+ return false
+
+ sourceNode.parent!!.removeChild(sourceNode)
+
+ sourceNode.name = newName
+ sourceNode.parent = newParent
+ sourceNode.uri = newUri
+
+ newParent.addChild(sourceNode)
+
+ return true
+ }
+
@Synchronized
private fun resolvePath(filepath: String): DocumentsNode? {
root ?: return null
@@ -288,15 +300,6 @@ 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 402a23857..61fcf9bfa 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
@@ -1,4 +1,4 @@
-// Copyright 2023 Citra Emulator Project
+// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -422,18 +422,6 @@ 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/PermissionsHandler.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.kt
index 8f1e9193f..6ea04c779 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.kt
@@ -8,6 +8,9 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
+import android.os.Build
+import android.provider.DocumentsContract
+import androidx.activity.result.ActivityResultLauncher
import androidx.preference.PreferenceManager
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
@@ -48,4 +51,17 @@ object PermissionsHandler {
fun setCitraDirectory(uriString: String?) =
preferences.edit().putString(CITRA_DIRECTORY, uriString).apply()
+
+ fun compatibleSelectDirectory(activityLauncher: ActivityResultLauncher) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ activityLauncher.launch(null)
+ } else {
+ val initialUri = DocumentsContract.buildRootUri(
+ "com.android.externalstorage.documents",
+ "primary"
+ )
+ activityLauncher.launch(initialUri)
+ }
+
+ }
}
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
new file mode 100644
index 000000000..1a859242d
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/RemovableStorageHelper.kt
@@ -0,0 +1,24 @@
+// 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 java.io.File
+
+object RemovableStorageHelper {
+ // This really shouldn't be necessary, but the Android API seemingly
+ // doesn't have a way of doing this?
+ // 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? {
+ var pathFile: File
+
+ pathFile = File("/mnt/media_rw/$idString");
+ if (pathFile.exists()) {
+ return pathFile.absolutePath
+ }
+
+ return null
+ }
+}
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index c79be56c8..4c4af45c2 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -76,10 +76,14 @@
Grant permission
Skip granting the notification permission?
Azahar won\'t be able to notify you of important information.
+ Missing Permissions
+ Azahar requires permission to manage files on this device in order to store and manage its data.\n\nPlease grant the \"Filesystem\" permission before continuing.
Camera
Grant the camera permission below to emulate the 3DS camera.
Microphone
Grant the microphone permission below to emulate the 3DS microphone.
+ Filesystem
+ Grant the filesystem permission below to allow Azahar to store files.
Permission denied
Skip selecting applications folder?
Software won\'t be displayed in the Applications list if a folder isn\'t selected.
@@ -100,6 +104,9 @@
You can\'t skip setting up the user folder
This step is required to allow Azahar to work. Please select a directory and then you can continue.
You have lost write permissions on your user data directory, where saves and other information are stored. This can happen after some app or Android updates. Please re-select the directory to regain permissions so you can continue.
+ Invalid Selection
+ The user directory selection was invalid.\nPlease re-select the user directory, ensuring that you navigate to it from the root of your device\'s storage.
+ Azahar has lost permission to manage files on this device. This can happen after some app or Android updates. Please re-grant this permission on the next screen to continue using the app.
https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage
Theme Settings
Configure your theme preferences for Azahar.
diff --git a/src/common/android_storage.cpp b/src/common/android_storage.cpp
index a18ecefac..542c1b172 100644
--- a/src/common/android_storage.cpp
+++ b/src/common/android_storage.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Citra Emulator Project
+// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -150,6 +150,20 @@ std::vector GetFilesName(const std::string& filepath) {
return vector;
}
+std::optional GetUserDirectory() {
+ 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));
+ auto result = env->GetStringUTFChars(j_user_directory, nullptr);
+ if (result == "") {
+ return std::nullopt;
+ }
+ return result;
+}
+
bool CopyFile(const std::string& source, const std::string& destination_path,
const std::string& destination_filename) {
if (copy_file == nullptr)
@@ -162,13 +176,13 @@ 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)
+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.c_str());
- jstring j_destination_path = env->NewStringUTF(filename.c_str());
- return env->CallStaticBooleanMethod(native_library, rename_file, j_source_path,
+ jstring j_source_path = env->NewStringUTF(source_path.c_str());
+ jstring j_destination_path = env->NewStringUTF(destination_path.c_str());
+ return env->CallStaticBooleanMethod(native_library, update_document_location, j_source_path,
j_destination_path);
}
diff --git a/src/common/android_storage.h b/src/common/android_storage.h
index 2ea0eb57c..aca474595 100644
--- a/src/common/android_storage.h
+++ b/src/common/android_storage.h
@@ -1,4 +1,4 @@
-// Copyright 2023 Citra Emulator Project
+// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -19,12 +19,16 @@
open_content_uri, "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I") \
V(GetFilesName, std::vector, (const std::string& filepath), get_files_name, \
"getFilesName", "(Ljava/lang/String;)[Ljava/lang/String;") \
+ V(GetUserDirectory, std::optional, (), get_user_directory, "getUserDirectory", \
+ "(Landroid/net/Uri;)Ljava/lang/String;") \
V(CopyFile, bool, \
(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")
#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 c740e8416..794e2080c 100644
--- a/src/common/file_util.cpp
+++ b/src/common/file_util.cpp
@@ -311,8 +311,13 @@ bool Rename(const std::string& srcFilename, const std::string& destFilename) {
Common::UTF8ToUTF16W(destFilename).c_str()) == 0)
return true;
#elif ANDROID
- if (AndroidStorage::RenameFile(srcFilename, std::string(GetFilename(destFilename))))
+ 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)
return true;