diff options
Diffstat (limited to 'src/android/app')
8 files changed, 363 insertions, 112 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index f0a6753a9..b1771b424 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -27,13 +27,13 @@ import android.view.MotionEvent import android.view.Surface import android.view.View import android.view.inputmethod.InputMethodManager +import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.navigation.fragment.NavHostFragment -import kotlin.math.roundToInt import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding @@ -44,8 +44,10 @@ import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.utils.ControllerMappingHelper import org.yuzu.yuzu_emu.utils.ForegroundService import org.yuzu.yuzu_emu.utils.InputHandler +import org.yuzu.yuzu_emu.utils.MemoryUtil import org.yuzu.yuzu_emu.utils.NfcReader import org.yuzu.yuzu_emu.utils.ThemeHelper +import kotlin.math.roundToInt class EmulationActivity : AppCompatActivity(), SensorEventListener { private lateinit var binding: ActivityEmulationBinding @@ -102,6 +104,19 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { inputHandler = InputHandler() inputHandler.initialize() + val memoryUtil = MemoryUtil(this) + if (memoryUtil.isLessThan(8, MemoryUtil.Gb)) { + Toast.makeText( + this, + getString( + R.string.device_memory_inadequate, + memoryUtil.getDeviceRAM(), + "8 ${getString(R.string.memory_gigabyte)}" + ), + Toast.LENGTH_LONG + ).show() + } + // Start a foreground service to prevent the app from getting killed in the background val startIntent = Intent(this, ForegroundService::class.java) startForegroundService(startIntent) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 6f8adbba5..5a36ffad4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -68,79 +68,109 @@ class HomeSettingsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mainActivity = requireActivity() as MainActivity - val optionsList: MutableList<HomeSetting> = mutableListOf( - HomeSetting( - R.string.advanced_settings, - R.string.settings_description, - R.drawable.ic_settings - ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") }, - HomeSetting( - R.string.open_user_folder, - R.string.open_user_folder_description, - R.drawable.ic_folder_open - ) { openFileManager() }, - HomeSetting( - R.string.preferences_theme, - R.string.theme_and_color_description, - R.drawable.ic_palette - ) { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") }, - HomeSetting( - R.string.install_gpu_driver, - R.string.install_gpu_driver_description, - R.drawable.ic_exit - ) { driverInstaller() }, - HomeSetting( - R.string.install_amiibo_keys, - R.string.install_amiibo_keys_description, - R.drawable.ic_nfc - ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }, - HomeSetting( - R.string.install_game_content, - R.string.install_game_content_description, - R.drawable.ic_system_update_alt - ) { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }, - HomeSetting( - R.string.select_games_folder, - R.string.select_games_folder_description, - R.drawable.ic_add - ) { - mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) - }, - HomeSetting( - R.string.manage_save_data, - R.string.import_export_saves_description, - R.drawable.ic_save - ) { - ImportExportSavesFragment().show( - parentFragmentManager, - ImportExportSavesFragment.TAG + val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply { + add( + HomeSetting( + R.string.advanced_settings, + R.string.settings_description, + R.drawable.ic_settings + ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } + ) + add( + HomeSetting( + R.string.open_user_folder, + R.string.open_user_folder_description, + R.drawable.ic_folder_open + ) { openFileManager() } + ) + add( + HomeSetting( + R.string.preferences_theme, + R.string.theme_and_color_description, + R.drawable.ic_palette + ) { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") } + ) + + if (GpuDriverHelper.supportsCustomDriverLoading()) { + add( + HomeSetting( + R.string.install_gpu_driver, + R.string.install_gpu_driver_description, + R.drawable.ic_exit + ) { driverInstaller() } ) - }, - HomeSetting( - R.string.install_prod_keys, - R.string.install_prod_keys_description, - R.drawable.ic_unlock - ) { mainActivity.getProdKey.launch(arrayOf("*/*")) }, - HomeSetting( - R.string.install_firmware, - R.string.install_firmware_description, - R.drawable.ic_firmware - ) { mainActivity.getFirmware.launch(arrayOf("application/zip")) }, - HomeSetting( - R.string.share_log, - R.string.share_log_description, - R.drawable.ic_log - ) { shareLog() }, - HomeSetting( - R.string.about, - R.string.about_description, - R.drawable.ic_info_outline - ) { - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - parentFragmentManager.primaryNavigationFragment?.findNavController() - ?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment) } - ) + + add( + HomeSetting( + R.string.install_amiibo_keys, + R.string.install_amiibo_keys_description, + R.drawable.ic_nfc + ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } + ) + add( + HomeSetting( + R.string.install_game_content, + R.string.install_game_content_description, + R.drawable.ic_system_update_alt + ) { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } + ) + add( + HomeSetting( + R.string.select_games_folder, + R.string.select_games_folder_description, + R.drawable.ic_add + ) { + mainActivity.getGamesDirectory.launch( + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data + ) + } + ) + add( + HomeSetting( + R.string.manage_save_data, + R.string.import_export_saves_description, + R.drawable.ic_save + ) { + ImportExportSavesFragment().show( + parentFragmentManager, + ImportExportSavesFragment.TAG + ) + } + ) + add( + HomeSetting( + R.string.install_prod_keys, + R.string.install_prod_keys_description, + R.drawable.ic_unlock + ) { mainActivity.getProdKey.launch(arrayOf("*/*")) } + ) + add( + HomeSetting( + R.string.install_firmware, + R.string.install_firmware_description, + R.drawable.ic_firmware + ) { mainActivity.getFirmware.launch(arrayOf("application/zip")) } + ) + add( + HomeSetting( + R.string.share_log, + R.string.share_log_description, + R.drawable.ic_log + ) { shareLog() } + ) + add( + HomeSetting( + R.string.about, + R.string.about_description, + R.drawable.ic_info_outline + ) { + exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + parentFragmentManager.primaryNavigationFragment?.findNavController() + ?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment) + } + ) + } if (!BuildConfig.PREMIUM) { optionsList.add( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt new file mode 100644 index 000000000..b29b627e9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R + +class LongMessageDialogFragment : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val titleId = requireArguments().getInt(TITLE) + val description = requireArguments().getString(DESCRIPTION) + val helpLinkId = requireArguments().getInt(HELP_LINK) + + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setPositiveButton(R.string.close, null) + .setTitle(titleId) + .setMessage(description) + + if (helpLinkId != 0) { + dialog.setNeutralButton(R.string.learn_more) { _, _ -> + openLink(getString(helpLinkId)) + } + } + + return dialog.show() + } + + private fun openLink(link: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) + startActivity(intent) + } + + companion object { + const val TAG = "LongMessageDialogFragment" + + private const val TITLE = "Title" + private const val DESCRIPTION = "Description" + private const val HELP_LINK = "Link" + + fun newInstance( + titleId: Int, + description: String, + helpLinkId: Int = 0 + ): LongMessageDialogFragment { + val dialog = LongMessageDialogFragment() + val bundle = Bundle() + bundle.apply { + putInt(TITLE, titleId) + putString(DESCRIPTION, description) + putInt(HELP_LINK, helpLinkId) + } + dialog.arguments = bundle + return dialog + } + } +} 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 cc1d87f1b..3086cfad3 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 @@ -4,6 +4,7 @@ package org.yuzu.yuzu_emu.ui.main import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.View import android.view.ViewGroup.MarginLayoutParams @@ -42,6 +43,7 @@ import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment +import org.yuzu.yuzu_emu.fragments.LongMessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel @@ -481,62 +483,110 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } } - val installGameUpdate = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { - if (it == null) { - return@registerForActivityResult - } - + val installGameUpdate = registerForActivityResult( + ActivityResultContracts.OpenMultipleDocuments() + ) { documents: List<Uri> -> + if (documents.isNotEmpty()) { IndeterminateProgressDialogFragment.newInstance( this@MainActivity, R.string.install_game_content ) { - val result = NativeLibrary.installFileToNand(it.toString()) + var installSuccess = 0 + var installOverwrite = 0 + var errorBaseGame = 0 + var errorExtension = 0 + var errorOther = 0 + var errorTotal = 0 lifecycleScope.launch { - withContext(Dispatchers.Main) { - when (result) { + documents.forEach { + when (NativeLibrary.installFileToNand(it.toString())) { NativeLibrary.InstallFileToNandResult.Success -> { - Toast.makeText( - applicationContext, - R.string.install_game_content_success, - Toast.LENGTH_SHORT - ).show() + installSuccess += 1 } NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { - Toast.makeText( - applicationContext, - R.string.install_game_content_success_overwrite, - Toast.LENGTH_SHORT - ).show() + installOverwrite += 1 } NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { - MessageDialogFragment.newInstance( - R.string.install_game_content_failure, - R.string.install_game_content_failure_base - ).show(supportFragmentManager, MessageDialogFragment.TAG) + errorBaseGame += 1 } NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { - MessageDialogFragment.newInstance( - R.string.install_game_content_failure, - R.string.install_game_content_failure_file_extension, - R.string.install_game_content_help_link - ).show(supportFragmentManager, MessageDialogFragment.TAG) + errorExtension += 1 } else -> { - MessageDialogFragment.newInstance( - R.string.install_game_content_failure, - R.string.install_game_content_failure_description, - R.string.install_game_content_help_link - ).show(supportFragmentManager, MessageDialogFragment.TAG) + errorOther += 1 } } } + withContext(Dispatchers.Main) { + val separator = System.getProperty("line.separator") ?: "\n" + val installResult = StringBuilder() + if (installSuccess > 0) { + installResult.append( + getString( + R.string.install_game_content_success_install, + installSuccess + ) + ) + installResult.append(separator) + } + if (installOverwrite > 0) { + installResult.append( + getString( + R.string.install_game_content_success_overwrite, + installOverwrite + ) + ) + installResult.append(separator) + } + errorTotal = errorBaseGame + errorExtension + errorOther + if (errorTotal > 0) { + installResult.append(separator) + installResult.append( + getString( + R.string.install_game_content_failed_count, + errorTotal + ) + ) + installResult.append(separator) + if (errorBaseGame > 0) { + installResult.append(separator) + installResult.append( + getString(R.string.install_game_content_failure_base) + ) + installResult.append(separator) + } + if (errorExtension > 0) { + installResult.append(separator) + installResult.append( + getString(R.string.install_game_content_failure_file_extension) + ) + installResult.append(separator) + } + if (errorOther > 0) { + installResult.append( + getString(R.string.install_game_content_failure_description) + ) + installResult.append(separator) + } + LongMessageDialogFragment.newInstance( + R.string.install_game_content_failure, + installResult.toString().trim(), + R.string.install_game_content_help_link + ).show(supportFragmentManager, LongMessageDialogFragment.TAG) + } else { + LongMessageDialogFragment.newInstance( + R.string.install_game_content_success, + installResult.toString().trim() + ).show(supportFragmentManager, LongMessageDialogFragment.TAG) + } + } } - return@newInstance result + return@newInstance installSuccess + installOverwrite + errorTotal }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) } + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index dad159481..1d4695a2a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -113,6 +113,8 @@ object GpuDriverHelper { initializeDriverParameters(context) } + external fun supportsCustomDriverLoading(): Boolean + // Parse the custom driver metadata to retrieve the name. val customDriverName: String? get() { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt new file mode 100644 index 000000000..18e5fa0b0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.app.ActivityManager +import android.content.Context +import org.yuzu.yuzu_emu.R +import java.util.Locale + +class MemoryUtil(val context: Context) { + + private val Long.floatForm: String + get() = String.format(Locale.ROOT, "%.2f", this.toDouble()) + + private fun bytesToSizeUnit(size: Long): String { + return when { + size < Kb -> "${size.floatForm} ${context.getString(R.string.memory_byte)}" + size < Mb -> "${(size / Kb).floatForm} ${context.getString(R.string.memory_kilobyte)}" + size < Gb -> "${(size / Mb).floatForm} ${context.getString(R.string.memory_megabyte)}" + size < Tb -> "${(size / Gb).floatForm} ${context.getString(R.string.memory_gigabyte)}" + size < Pb -> "${(size / Tb).floatForm} ${context.getString(R.string.memory_terabyte)}" + size < Eb -> "${(size / Pb).floatForm} ${context.getString(R.string.memory_petabyte)}" + else -> "${(size / Eb).floatForm} ${context.getString(R.string.memory_exabyte)}" + } + } + + private val totalMemory = + with(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) { + val memInfo = ActivityManager.MemoryInfo() + getMemoryInfo(memInfo) + memInfo.totalMem + } + + fun isLessThan(minimum: Int, size: Long): Boolean { + return when (size) { + Kb -> totalMemory < Mb && totalMemory < minimum + Mb -> totalMemory < Gb && (totalMemory / Mb) < minimum + Gb -> totalMemory < Tb && (totalMemory / Gb) < minimum + Tb -> totalMemory < Pb && (totalMemory / Tb) < minimum + Pb -> totalMemory < Eb && (totalMemory / Pb) < minimum + Eb -> totalMemory / Eb < minimum + else -> totalMemory < Kb && totalMemory < minimum + } + } + + fun getDeviceRAM(): String { + return bytesToSizeUnit(totalMemory) + } + + companion object { + const val Kb: Long = 1024 + const val Mb = Kb * 1024 + const val Gb = Mb * 1024 + const val Tb = Gb * 1024 + const val Pb = Tb * 1024 + const val Eb = Pb * 1024 + } +} diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index f9617202b..f4fed0886 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -237,6 +237,7 @@ public: m_software_keyboard = android_keyboard.get(); m_system.SetShuttingDown(false); m_system.ApplySettings(); + Settings::LogSettings(); m_system.HIDCore().ReloadInputDevices(); m_system.SetAppletFrontendSet({ nullptr, // Amiibo Settings @@ -560,6 +561,26 @@ void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver( GetJString(env, custom_driver_name), GetJString(env, file_redirect_dir)); } +[[maybe_unused]] static bool CheckKgslPresent() { + constexpr auto KgslPath{"/dev/kgsl-3d0"}; + + return access(KgslPath, F_OK) == 0; +} + +[[maybe_unused]] bool SupportsCustomDriver() { + return android_get_device_api_level() >= 28 && CheckKgslPresent(); +} + +jboolean JNICALL Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_supportsCustomDriverLoading( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject instance) { +#ifdef ARCHITECTURE_arm64 + // If the KGSL device exists custom drivers can be loaded using adrenotools + return SupportsCustomDriver(); +#else + return false; +#endif +} + jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadKeys(JNIEnv* env, [[maybe_unused]] jclass clazz) { Core::Crypto::KeyManager::Instance().ReloadKeys(); diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index cc1d8c39d..21805d274 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -104,12 +104,14 @@ <string name="share_log_missing">No log file found</string> <string name="install_game_content">Install game content</string> <string name="install_game_content_description">Install game updates or DLC</string> - <string name="install_game_content_failure">Error installing file to NAND</string> - <string name="install_game_content_failure_description">Game content installation failed. Please ensure content is valid and that the prod.keys file is installed.</string> - <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts. Please select an update or DLC instead.</string> - <string name="install_game_content_failure_file_extension">The selected file type is not supported. Only NSP and XCI content is supported for this action. Please verify the game content is valid.</string> - <string name="install_game_content_success">Game content installed successfully</string> - <string name="install_game_content_success_overwrite">Game content was overwritten successfully</string> + <string name="install_game_content_failure">Error installing file(s) to NAND</string> + <string name="install_game_content_failure_description">Please ensure content(s) are valid and that the prod.keys file is installed.</string> + <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts.</string> + <string name="install_game_content_failure_file_extension">Only NSP and XCI content is supported. Please verify the game content(s) are valid.</string> + <string name="install_game_content_failed_count">%1$d installation error(s)</string> + <string name="install_game_content_success">Game content(s) installed successfully</string> + <string name="install_game_content_success_install">%1$d installed successfully</string> + <string name="install_game_content_success_overwrite">%1$d overwritten successfully</string> <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string> <!-- About screen strings --> @@ -270,6 +272,7 @@ <string name="fatal_error">Fatal Error</string> <string name="fatal_error_message">A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs.</string> <string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string> + <string name="device_memory_inadequate">Device RAM: %1$s\nRecommended: %2$s</string> <!-- Region Names --> <string name="region_japan">Japan</string> @@ -300,6 +303,15 @@ <string name="language_traditional_chinese">Traditional Chinese (正體中文)</string> <string name="language_brazilian_portuguese">Brazilian Portuguese (Português do Brasil)</string> + <!-- Memory Sizes --> + <string name="memory_byte">Byte</string> + <string name="memory_kilobyte">KB</string> + <string name="memory_megabyte">MB</string> + <string name="memory_gigabyte">GB</string> + <string name="memory_terabyte">TB</string> + <string name="memory_petabyte">PB</string> + <string name="memory_exabyte">EB</string> + <!-- Renderer APIs --> <string name="renderer_vulkan">Vulkan</string> <string name="renderer_none">None</string> |