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;