summaryrefslogtreecommitdiffstats
path: root/src/android/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt28
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt219
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt12
-rw-r--r--src/android/app/src/main/jni/android_config.cpp6
-rw-r--r--src/android/app/src/main/jni/android_config.h1
-rw-r--r--src/android/app/src/main/jni/native.cpp22
-rw-r--r--src/android/app/src/main/res/values/arrays.xml12
-rw-r--r--src/android/app/src/main/res/values/strings.xml16
13 files changed, 324 insertions, 29 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 010c44951..b7556e353 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -548,6 +548,15 @@ object NativeLibrary {
external fun getSavePath(programId: String): String
/**
+ * Gets the root save directory for the default profile as either
+ * /user/save/account/<user id raw string> or /user/save/000...000/<user id>
+ *
+ * @param future If true, returns the /user/save/account/... directory
+ * @return Save data path that may not exist yet
+ */
+ external fun getDefaultProfileSaveDataRoot(future: Boolean): String
+
+ /**
* Adds a file to the manual filesystem provider in our EmulationSession instance
* @param path Path to the file we're adding. Can be a string representation of a [Uri] or
* a normal path
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index 43caac989..fee80bb21 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -79,7 +79,18 @@ object Settings {
const val PREF_THEME_MODE = "ThemeMode"
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
- const val LayoutOption_Unspecified = 0
- const val LayoutOption_MobilePortrait = 4
- const val LayoutOption_MobileLandscape = 5
+ enum class EmulationOrientation(val int: Int) {
+ Unspecified(0),
+ SensorLandscape(5),
+ Landscape(1),
+ ReverseLandscape(2),
+ SensorPortrait(6),
+ Portrait(4),
+ ReversePortrait(3);
+
+ companion object {
+ fun from(int: Int): EmulationOrientation =
+ entries.firstOrNull { it.int == int } ?: Unspecified
+ }
+ }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 510b2b5eb..9efc1705d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -50,6 +50,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.model.Settings.EmulationOrientation
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game
@@ -99,6 +100,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ updateOrientation()
val intentUri: Uri? = requireActivity().intent.data
var intentGame: Game? = null
@@ -458,13 +460,23 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@SuppressLint("SourceLockedOrientationActivity")
private fun updateOrientation() {
emulationActivity?.let {
- it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) {
- Settings.LayoutOption_MobileLandscape ->
+ val orientationSetting =
+ EmulationOrientation.from(IntSetting.RENDERER_SCREEN_LAYOUT.getInt())
+ it.requestedOrientation = when (orientationSetting) {
+ EmulationOrientation.Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ EmulationOrientation.SensorLandscape ->
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
- Settings.LayoutOption_MobilePortrait ->
- ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
- Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
- else -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+
+ EmulationOrientation.Landscape -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ EmulationOrientation.ReverseLandscape ->
+ ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
+
+ EmulationOrientation.SensorPortrait ->
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
+
+ EmulationOrientation.Portrait -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ EmulationOrientation.ReversePortrait ->
+ ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
}
}
}
@@ -651,7 +663,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@SuppressLint("SourceLockedOrientationActivity")
private fun startConfiguringControls() {
// Lock the current orientation to prevent editing inconsistencies
- if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
+ if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) {
emulationActivity?.let {
it.requestedOrientation =
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
@@ -669,7 +681,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.doneControlConfig.visibility = View.GONE
binding.surfaceInputOverlay.setIsInEditMode(false)
// Unlock the orientation if it was locked for editing
- if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
+ if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) {
emulationActivity?.let {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index b1d3c0040..b04d1208f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -445,7 +445,8 @@ class GamePropertiesFragment : Fragment() {
val zipResult = FileUtil.zipFromInternalStorage(
File(saveLocation),
saveLocation.replaceAfterLast("/", ""),
- BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
+ BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)),
+ compression = false
)
return@newInstance when (zipResult) {
TaskState.Completed -> getString(R.string.export_success)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 569727b90..5b4bf2c9f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -7,20 +7,39 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.InstallableAdapter
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.Installable
+import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.ui.main.MainActivity
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.FileUtil
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.File
+import java.math.BigInteger
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
class InstallableFragment : Fragment() {
private var _binding: FragmentInstallablesBinding? = null
@@ -56,6 +75,17 @@ class InstallableFragment : Fragment() {
binding.root.findNavController().popBackStack()
}
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ homeViewModel.openImportSaves.collect {
+ if (it) {
+ importSaves.launch(arrayOf("application/zip"))
+ homeViewModel.setOpenImportSaves(false)
+ }
+ }
+ }
+ }
+
val installables = listOf(
Installable(
R.string.user_data,
@@ -64,6 +94,43 @@ class InstallableFragment : Fragment() {
export = { mainActivity.exportUserData.launch("export.zip") }
),
Installable(
+ R.string.manage_save_data,
+ R.string.manage_save_data_description,
+ install = {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.import_save_warning,
+ descriptionId = R.string.import_save_warning_description,
+ positiveAction = { homeViewModel.setOpenImportSaves(true) }
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ },
+ export = {
+ val oldSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(false)
+ )
+ val futureSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(true)
+ )
+ if (!oldSaveDataFolder.exists() && !futureSaveDataFolder.exists()) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ R.string.no_save_data_found,
+ Toast.LENGTH_SHORT
+ ).show()
+ return@Installable
+ } else {
+ exportSaves.launch(
+ "${getString(R.string.save_data)} " +
+ LocalDateTime.now().format(
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
+ )
+ )
+ }
+ }
+ ),
+ Installable(
R.string.install_game_content,
R.string.install_game_content_description,
install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
@@ -121,4 +188,156 @@ class InstallableFragment : Fragment() {
windowInsets
}
+
+ private val importSaves =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ val inputZip = requireContext().contentResolver.openInputStream(result)
+ val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
+ cacheSaveDir.mkdir()
+
+ if (inputZip == null) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.save_files_importing,
+ false
+ ) {
+ try {
+ FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+ val files = cacheSaveDir.listFiles()
+ var successfulImports = 0
+ var failedImports = 0
+ if (files != null) {
+ for (file in files) {
+ if (file.isDirectory) {
+ val baseSaveDir =
+ NativeLibrary.getSavePath(BigInteger(file.name, 16).toString())
+ if (baseSaveDir.isEmpty()) {
+ failedImports++
+ continue
+ }
+
+ val internalSaveFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand$baseSaveDir"
+ )
+ internalSaveFolder.deleteRecursively()
+ internalSaveFolder.mkdir()
+ file.copyRecursively(target = internalSaveFolder, overwrite = true)
+ successfulImports++
+ }
+ }
+ }
+
+ withContext(Dispatchers.Main) {
+ if (successfulImports == 0) {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.save_file_invalid_zip_structure,
+ descriptionId = R.string.save_file_invalid_zip_structure_description
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ return@withContext
+ }
+ val successString = if (failedImports > 0) {
+ """
+ ${
+ requireContext().resources.getQuantityString(
+ R.plurals.saves_import_success,
+ successfulImports,
+ successfulImports
+ )
+ }
+ ${
+ requireContext().resources.getQuantityString(
+ R.plurals.saves_import_failed,
+ failedImports,
+ failedImports
+ )
+ }
+ """
+ } else {
+ requireContext().resources.getQuantityString(
+ R.plurals.saves_import_success,
+ successfulImports,
+ successfulImports
+ )
+ }
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.import_complete,
+ descriptionString = successString
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ }
+
+ cacheSaveDir.deleteRecursively()
+ } catch (e: Exception) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+
+ private val exportSaves = registerForActivityResult(
+ ActivityResultContracts.CreateDocument("application/zip")
+ ) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.save_files_exporting,
+ false
+ ) {
+ val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
+ cacheSaveDir.mkdir()
+
+ val oldSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(false)
+ )
+ if (oldSaveDataFolder.exists()) {
+ oldSaveDataFolder.copyRecursively(cacheSaveDir)
+ }
+
+ val futureSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(true)
+ )
+ if (futureSaveDataFolder.exists()) {
+ futureSaveDataFolder.copyRecursively(cacheSaveDir)
+ }
+
+ val saveFilesTotal = cacheSaveDir.listFiles()?.size ?: 0
+ if (saveFilesTotal == 0) {
+ cacheSaveDir.deleteRecursively()
+ return@newInstance getString(R.string.no_save_data_found)
+ }
+
+ val zipResult = FileUtil.zipFromInternalStorage(
+ cacheSaveDir,
+ cacheSaveDir.path,
+ BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
+ )
+ cacheSaveDir.deleteRecursively()
+
+ return@newInstance when (zipResult) {
+ TaskState.Completed -> getString(R.string.export_success)
+ TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
+ }
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index d19f20dc2..5ae05b5cc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -167,13 +167,14 @@ class GamesViewModel : ViewModel() {
}
}
- fun onCloseGameFoldersFragment() =
+ fun onCloseGameFoldersFragment() {
+ NativeConfig.saveGlobalConfig()
viewModelScope.launch {
withContext(Dispatchers.IO) {
- NativeConfig.saveGlobalConfig()
getGameDirs(true)
}
}
+ }
private fun getGameDirs(reloadList: Boolean = false) {
val gameDirs = NativeConfig.getGameDirs()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index b4117d761..622ae996e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -625,7 +625,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
File(DirectoryInitialization.userDirectory!!),
DirectoryInitialization.userDirectory!!,
BufferedOutputStream(contentResolver.openOutputStream(result)),
- taskViewModel.cancelled
+ taskViewModel.cancelled,
+ compression = false
)
return@newInstance when (zipResult) {
TaskState.Completed -> getString(R.string.user_data_export_success)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index 00c6bf90e..132f002fb 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -21,6 +21,7 @@ import org.yuzu.yuzu_emu.model.TaskState
import java.io.BufferedOutputStream
import java.lang.NullPointerException
import java.nio.charset.StandardCharsets
+import java.util.zip.Deflater
import java.util.zip.ZipOutputStream
import kotlin.IllegalStateException
@@ -312,15 +313,23 @@ object FileUtil {
* @param inputFile File representation of the item that will be zipped
* @param rootDir Directory containing the inputFile
* @param outputStream Stream where the zip file will be output
+ * @param cancelled [StateFlow] that reports whether this process has been cancelled
+ * @param compression Disables compression if true
*/
fun zipFromInternalStorage(
inputFile: File,
rootDir: String,
outputStream: BufferedOutputStream,
- cancelled: StateFlow<Boolean>? = null
+ cancelled: StateFlow<Boolean>? = null,
+ compression: Boolean = true
): TaskState {
try {
ZipOutputStream(outputStream).use { zos ->
+ if (!compression) {
+ zos.setMethod(ZipOutputStream.DEFLATED)
+ zos.setLevel(Deflater.NO_COMPRESSION)
+ }
+
inputFile.walkTopDown().forEach { file ->
if (cancelled?.value == true) {
return TaskState.Cancelled
@@ -338,6 +347,7 @@ object FileUtil {
}
}
} catch (e: Exception) {
+ Log.error("[FileUtil] Failed creating zip file - ${e.message}")
return TaskState.Failed
}
return TaskState.Completed
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp
index c86aa1c39..08aed3216 100644
--- a/src/android/app/src/main/jni/android_config.cpp
+++ b/src/android/app/src/main/jni/android_config.cpp
@@ -14,12 +14,6 @@ AndroidConfig::AndroidConfig(const std::string& config_name, ConfigType config_t
}
}
-AndroidConfig::~AndroidConfig() {
- if (global) {
- AndroidConfig::SaveAllValues();
- }
-}
-
void AndroidConfig::ReloadAllValues() {
Reload();
ReadAndroidValues();
diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h
index d83852de9..693e1e3f0 100644
--- a/src/android/app/src/main/jni/android_config.h
+++ b/src/android/app/src/main/jni/android_config.h
@@ -9,7 +9,6 @@ class AndroidConfig final : public Config {
public:
explicit AndroidConfig(const std::string& config_name = "config",
ConfigType config_type = ConfigType::GlobalConfig);
- ~AndroidConfig() override;
void ReloadAllValues() override;
void SaveAllValues() override;
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 0c1db7d46..136c8dee6 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -45,15 +45,15 @@
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h"
-#include "core/hid/emulated_controller.h"
-#include "core/hid/hid_core.h"
-#include "core/hid/hid_types.h"
#include "core/hle/service/am/applet_ae.h"
#include "core/hle/service/am/applet_oe.h"
#include "core/hle/service/am/applets/applets.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#include "frontend_common/config.h"
+#include "hid_core/frontend/emulated_controller.h"
+#include "hid_core/hid_core.h"
+#include "hid_core/hid_types.h"
#include "jni/android_common/android_common.h"
#include "jni/id_cache.h"
#include "jni/native.h"
@@ -862,6 +862,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env,
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
jstring jprogramId) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
+ if (program_id == 0) {
+ return ToJString(env, "");
+ }
auto& system = EmulationSession::GetInstance().System();
@@ -880,6 +883,19 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j
return ToJString(env, user_save_data_path);
}
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultProfileSaveDataRoot(JNIEnv* env,
+ jobject jobj,
+ jboolean jfuture) {
+ Service::Account::ProfileManager manager;
+ // TODO: Pass in a selected user once we get the relevant UI working
+ const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
+ ASSERT(user_id);
+
+ const auto user_save_data_root =
+ FileSys::SaveDataFactory::GetUserGameSaveDataRoot(user_id->AsU128(), jfuture);
+ return ToJString(env, user_save_data_root);
+}
+
void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj,
jstring jpath) {
EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath));
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 45d57c3ea..0363ff3b6 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -118,15 +118,23 @@
</integer-array>
<string-array name="rendererScreenLayoutNames">
+ <item>@string/screen_layout_auto</item>
+ <item>@string/screen_layout_sensor_landscape</item>
<item>@string/screen_layout_landscape</item>
+ <item>@string/screen_layout_reverse_landscape</item>
+ <item>@string/screen_layout_sensor_portrait</item>
<item>@string/screen_layout_portrait</item>
- <item>@string/screen_layout_auto</item>
+ <item>@string/screen_layout_reverse_portrait</item>
</string-array>
<integer-array name="rendererScreenLayoutValues">
+ <item>0</item>
<item>5</item>
+ <item>1</item>
+ <item>2</item>
+ <item>6</item>
<item>4</item>
- <item>0</item>
+ <item>3</item>
</integer-array>
<string-array name="rendererAspectRatioNames">
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 1bedcb1ef..3bb92ad67 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -133,6 +133,15 @@
<string name="add_game_folder">Add game folder</string>
<string name="folder_already_added">This folder was already added!</string>
<string name="game_folder_properties">Game folder properties</string>
+ <plurals name="saves_import_failed">
+ <item quantity="one">Failed to import %d save</item>
+ <item quantity="other">Failed to import %d saves</item>
+ </plurals>
+ <plurals name="saves_import_success">
+ <item quantity="one">Successfully imported %d save</item>
+ <item quantity="other">Successfully imported %d saves</item>
+ </plurals>
+ <string name="no_save_data_found">No save data found</string>
<!-- Applet launcher strings -->
<string name="applets">Applet launcher</string>
@@ -276,6 +285,7 @@
<string name="global">Global</string>
<string name="custom">Custom</string>
<string name="notice">Notice</string>
+ <string name="import_complete">Import complete</string>
<!-- GPU driver installation -->
<string name="select_gpu_driver">Select GPU driver</string>
@@ -463,9 +473,13 @@
<string name="anti_aliasing_smaa">SMAA</string>
<!-- Screen Layouts -->
+ <string name="screen_layout_auto">Auto</string>
+ <string name="screen_layout_sensor_landscape">Sensor landscape</string>
<string name="screen_layout_landscape">Landscape</string>
+ <string name="screen_layout_reverse_landscape">Reverse landscape</string>
+ <string name="screen_layout_sensor_portrait">Sensor portrait</string>
<string name="screen_layout_portrait">Portrait</string>
- <string name="screen_layout_auto">Auto</string>
+ <string name="screen_layout_reverse_portrait">Reverse portrait</string>
<!-- Aspect Ratios -->
<string name="ratio_default">Default (16:9)</string>