summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
Diffstat (limited to 'src/android')
-rw-r--r--src/android/.gitignore3
-rw-r--r--src/android/app/build.gradle.kts18
-rw-r--r--src/android/app/src/main/AndroidManifest.xml1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt26
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt117
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt49
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt186
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt75
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt95
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt89
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt213
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt79
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt138
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt158
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt328
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt164
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt228
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt112
-rw-r--r--src/android/app/src/main/jni/emu_window/emu_window.cpp14
-rw-r--r--src/android/app/src/main/jni/native.cpp37
-rw-r--r--src/android/app/src/main/res/drawable/ic_build.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_delete.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_export.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_import.xml9
-rw-r--r--src/android/app/src/main/res/layout/activity_settings.xml2
-rw-r--r--src/android/app/src/main/res/layout/card_driver_option.xml89
-rw-r--r--src/android/app/src/main/res/layout/card_installable.xml71
-rw-r--r--src/android/app/src/main/res/layout/dialog_progress_bar.xml28
-rw-r--r--src/android/app/src/main/res/layout/fragment_driver_manager.xml48
-rw-r--r--src/android/app/src/main/res/layout/fragment_emulation.xml5
-rw-r--r--src/android/app/src/main/res/layout/fragment_installables.xml31
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml14
-rw-r--r--src/android/app/src/main/res/values-de/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-es/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-fr/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-it/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-ja/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-ko/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-nb/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-pl/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-pt-rBR/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-pt-rPT/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-ru/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-uk/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-w600dp/integers.xml6
-rw-r--r--src/android/app/src/main/res/values-zh-rCN/strings.xml3
-rw-r--r--src/android/app/src/main/res/values-zh-rTW/strings.xml3
-rw-r--r--src/android/app/src/main/res/values/dimens.xml2
-rw-r--r--src/android/app/src/main/res/values/integers.xml2
-rw-r--r--src/android/app/src/main/res/values/strings.xml31
-rw-r--r--src/android/build.gradle.kts4
63 files changed, 1957 insertions, 708 deletions
diff --git a/src/android/.gitignore b/src/android/.gitignore
index 121cc8484..ff7121acd 100644
--- a/src/android/.gitignore
+++ b/src/android/.gitignore
@@ -63,3 +63,6 @@ fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
+
+# Autogenerated library for vulkan validation layers
+libVkLayer_khronos_validation.so
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index 431f899b3..ac43d84b7 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -27,7 +27,7 @@ android {
namespace = "org.yuzu.yuzu_emu"
compileSdkVersion = "android-34"
- ndkVersion = "25.2.9519653"
+ ndkVersion = "26.1.10909125"
buildFeatures {
viewBinding = true
@@ -203,23 +203,23 @@ ktlint {
}
dependencies {
- implementation("androidx.core:core-ktx:1.10.1")
+ implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
- implementation("androidx.recyclerview:recyclerview:1.3.0")
+ implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
- implementation("androidx.fragment:fragment-ktx:1.6.0")
+ implementation("androidx.fragment:fragment-ktx:1.6.1")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("com.google.android.material:material:1.9.0")
- implementation("androidx.preference:preference:1.2.0")
- implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
+ implementation("androidx.preference:preference-ktx:1.2.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1")
- implementation("androidx.window:window:1.1.0")
+ implementation("androidx.window:window:1.2.0-beta03")
implementation("org.ini4j:ini4j:0.5.4")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
- implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
- implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
+ implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
}
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 832c08e15..a67351727 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -28,7 +28,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:appCategory="game"
android:localeConfig="@xml/locales_config"
android:banner="@drawable/tv_banner"
- android:extractNativeLibs="true"
android:fullBackupContent="@xml/data_extraction_rules"
android:dataExtractionRules="@xml/data_extraction_rules_api_31"
android:enableOnBackInvokedCallback="true">
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 21f67f32a..115f72710 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
@@ -15,13 +15,9 @@ import androidx.annotation.Keep
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
-import org.yuzu.yuzu_emu.YuzuApplication.Companion.appContext
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath
-import org.yuzu.yuzu_emu.utils.FileUtil.exists
-import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
-import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory
-import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
+import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
@@ -75,7 +71,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.openContentUri(path, openmode)
} else {
- openContentUri(appContext, path, openmode)
+ FileUtil.openContentUri(path, openmode)
}
}
@@ -85,7 +81,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.getFileSize(path)
} else {
- getFileSize(appContext, path)
+ FileUtil.getFileSize(path)
}
}
@@ -95,7 +91,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.exists(path)
} else {
- exists(appContext, path)
+ FileUtil.exists(path)
}
}
@@ -105,7 +101,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.isDirectory(path)
} else {
- isDirectory(appContext, path)
+ FileUtil.isDirectory(path)
}
}
@@ -247,7 +243,12 @@ object NativeLibrary {
external fun setAppDirectory(directory: String)
- external fun installFileToNand(filename: String): Int
+ /**
+ * Installs a nsp or xci file to nand
+ * @param filename String representation of file uri
+ * @param extension Lowercase string representation of file extension without "."
+ */
+ external fun installFileToNand(filename: String, extension: String): Int
external fun initializeGpuDriver(
hookLibDir: String?,
@@ -512,6 +513,11 @@ object NativeLibrary {
external fun submitInlineKeyboardInput(key_code: Int)
/**
+ * Creates a generic user directory if it doesn't exist already
+ */
+ external fun initializeEmptyUserDirectory()
+
+ /**
* Button type for use in onTouchEvent
*/
object ButtonType {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 9561748cb..8c053670c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -47,7 +47,7 @@ class YuzuApplication : Application() {
application = this
documentsTree = DocumentsTree()
DirectoryInitialization.start()
- GpuDriverHelper.initializeDriverParameters(applicationContext)
+ GpuDriverHelper.initializeDriverParameters()
NativeLibrary.logDeviceInfo()
createNotificationChannels()
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 d4ae39661..e96a2059b 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
@@ -3,6 +3,7 @@
package org.yuzu.yuzu_emu.activities
+import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.app.PictureInPictureParams
@@ -397,6 +398,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
}
}
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
@@ -409,7 +411,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
addAction(actionMute)
addAction(actionUnmute)
}.also {
- registerReceiver(pictureInPictureReceiver, it)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(pictureInPictureReceiver, it, RECEIVER_EXPORTED)
+ } else {
+ registerReceiver(pictureInPictureReceiver, it)
+ }
}
} else {
try {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
new file mode 100644
index 000000000..0e818cab9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -0,0 +1,117 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
+
+class DriverAdapter(private val driverViewModel: DriverViewModel) :
+ ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
+ AsyncDifferConfig.Builder(DiffCallback()).build()
+ ) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
+ val binding =
+ CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return DriverViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = currentList.size
+
+ override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
+ holder.bind(currentList[position])
+
+ private fun onSelectDriver(position: Int) {
+ driverViewModel.setSelectedDriverIndex(position)
+ notifyItemChanged(driverViewModel.previouslySelectedDriver)
+ notifyItemChanged(driverViewModel.selectedDriver)
+ }
+
+ private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) {
+ if (driverViewModel.selectedDriver > position) {
+ driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
+ }
+ if (GpuDriverHelper.customDriverData == driverData.second) {
+ driverViewModel.setSelectedDriverIndex(0)
+ }
+ driverViewModel.driversToDelete.add(driverData.first)
+ driverViewModel.removeDriver(driverData)
+ notifyItemRemoved(position)
+ notifyItemChanged(driverViewModel.selectedDriver)
+ }
+
+ inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ private lateinit var driverData: Pair<String, GpuDriverMetadata>
+
+ fun bind(driverData: Pair<String, GpuDriverMetadata>) {
+ this.driverData = driverData
+ val driver = driverData.second
+
+ binding.apply {
+ radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
+ root.setOnClickListener {
+ onSelectDriver(bindingAdapterPosition)
+ }
+ buttonDelete.setOnClickListener {
+ onDeleteDriver(driverData, bindingAdapterPosition)
+ }
+
+ // Delay marquee by 3s
+ title.postDelayed(
+ {
+ title.isSelected = true
+ title.ellipsize = TextUtils.TruncateAt.MARQUEE
+ version.isSelected = true
+ version.ellipsize = TextUtils.TruncateAt.MARQUEE
+ description.isSelected = true
+ description.ellipsize = TextUtils.TruncateAt.MARQUEE
+ },
+ 3000
+ )
+ if (driver.name == null) {
+ title.setText(R.string.system_gpu_driver)
+ description.text = ""
+ version.text = ""
+ version.visibility = View.GONE
+ description.visibility = View.GONE
+ buttonDelete.visibility = View.GONE
+ } else {
+ title.text = driver.name
+ version.text = driver.version
+ description.text = driver.description
+ version.visibility = View.VISIBLE
+ description.visibility = View.VISIBLE
+ buttonDelete.visibility = View.VISIBLE
+ }
+ }
+ }
+ }
+
+ private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() {
+ override fun areItemsTheSame(
+ oldItem: Pair<String, GpuDriverMetadata>,
+ newItem: Pair<String, GpuDriverMetadata>
+ ): Boolean {
+ return oldItem.first == newItem.first
+ }
+
+ override fun areContentsTheSame(
+ oldItem: Pair<String, GpuDriverMetadata>,
+ newItem: Pair<String, GpuDriverMetadata>
+ ): Boolean {
+ return oldItem.second == newItem.second
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
index 1675627a1..58ce343f4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
@@ -49,6 +49,7 @@ class HomeSettingAdapter(
holder.option.onClick.invoke()
} else {
MessageDialogFragment.newInstance(
+ activity,
titleId = holder.option.disabledTitleId,
descriptionId = holder.option.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
new file mode 100644
index 000000000..e960fbaab
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
+import org.yuzu.yuzu_emu.model.Installable
+
+class InstallableAdapter(private val installables: List<Installable>) :
+ RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): InstallableAdapter.InstallableViewHolder {
+ val binding =
+ CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return InstallableViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = installables.size
+
+ override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) =
+ holder.bind(installables[position])
+
+ inner class InstallableViewHolder(val binding: CardInstallableBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ lateinit var installable: Installable
+
+ fun bind(installable: Installable) {
+ this.installable = installable
+
+ binding.title.setText(installable.titleId)
+ binding.description.setText(installable.descriptionId)
+
+ if (installable.install != null) {
+ binding.buttonInstall.visibility = View.VISIBLE
+ binding.buttonInstall.setOnClickListener { installable.install.invoke() }
+ }
+ if (installable.export != null) {
+ binding.buttonExport.visibility = View.VISIBLE
+ binding.buttonExport.setOnClickListener { installable.export.invoke() }
+ }
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index 4d2f2f604..c73edd50e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -21,6 +21,7 @@ import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.NativeLibrary
import java.io.IOException
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@@ -168,7 +169,7 @@ class SettingsActivity : AppCompatActivity() {
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
- Settings.settingsList.forEach { it.reset() }
+ NativeLibrary.reloadSettings()
Toast.makeText(
applicationContext,
@@ -181,12 +182,14 @@ class SettingsActivity : AppCompatActivity() {
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.navigationBarShade
- ) { view: View, windowInsets: WindowInsetsCompat ->
+ ) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- val mlpShade = view.layoutParams as MarginLayoutParams
- mlpShade.height = barInsets.bottom
- view.layoutParams = mlpShade
+ // The only situation where we care to have a nav bar shade is when it's at the bottom
+ // of the screen where scrolling list elements can go behind it.
+ val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
+ mlpNavShade.height = barInsets.bottom
+ binding.navigationBarShade.layoutParams = mlpNavShade
windowInsets
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
new file mode 100644
index 000000000..df21d74b2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -0,0 +1,186 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+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.lifecycleScope
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.DriverAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import java.io.File
+import java.io.IOException
+
+class DriverManagerFragment : Fragment() {
+ private var _binding: FragmentDriverManagerBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentDriverManagerBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ if (!driverViewModel.isInteractionAllowed) {
+ DriversLoadingDialogFragment().show(
+ childFragmentManager,
+ DriversLoadingDialogFragment.TAG
+ )
+ }
+
+ binding.toolbarDrivers.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
+ binding.buttonInstall.setOnClickListener {
+ getDriver.launch(arrayOf("application/zip"))
+ }
+
+ binding.listDrivers.apply {
+ layoutManager = GridLayoutManager(
+ requireContext(),
+ resources.getInteger(R.integer.grid_columns)
+ )
+ adapter = DriverAdapter(driverViewModel)
+ }
+
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ driverViewModel.driverList.collectLatest {
+ (binding.listDrivers.adapter as DriverAdapter).submitList(it)
+ }
+ }
+ launch {
+ driverViewModel.newDriverInstalled.collect {
+ if (_binding != null && it) {
+ (binding.listDrivers.adapter as DriverAdapter).apply {
+ notifyItemChanged(driverViewModel.previouslySelectedDriver)
+ notifyItemChanged(driverViewModel.selectedDriver)
+ driverViewModel.setNewDriverInstalled(false)
+ }
+ }
+ }
+ }
+ }
+
+ setInsets()
+ }
+
+ // Start installing requested driver
+ override fun onStop() {
+ super.onStop()
+ driverViewModel.onCloseDriverManager()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.root
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpAppBar = binding.toolbarDrivers.layoutParams as ViewGroup.MarginLayoutParams
+ mlpAppBar.leftMargin = leftInsets
+ mlpAppBar.rightMargin = rightInsets
+ binding.toolbarDrivers.layoutParams = mlpAppBar
+
+ val mlplistDrivers = binding.listDrivers.layoutParams as ViewGroup.MarginLayoutParams
+ mlplistDrivers.leftMargin = leftInsets
+ mlplistDrivers.rightMargin = rightInsets
+ binding.listDrivers.layoutParams = mlplistDrivers
+
+ val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
+ val mlpFab =
+ binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
+ mlpFab.leftMargin = leftInsets + fabSpacing
+ mlpFab.rightMargin = rightInsets + fabSpacing
+ mlpFab.bottomMargin = barInsets.bottom + fabSpacing
+ binding.buttonInstall.layoutParams = mlpFab
+
+ binding.listDrivers.updatePadding(
+ bottom = barInsets.bottom +
+ resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+ )
+
+ windowInsets
+ }
+
+ private val getDriver =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.installing_driver,
+ false
+ ) {
+ val driverPath =
+ "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}"
+ val driverFile = File(driverPath)
+
+ // Ignore file exceptions when a user selects an invalid zip
+ try {
+ if (!GpuDriverHelper.copyDriverToInternalStorage(result)) {
+ throw IOException("Driver failed validation!")
+ }
+ } catch (_: IOException) {
+ if (driverFile.exists()) {
+ driverFile.delete()
+ }
+ return@newInstance getString(R.string.select_gpu_driver_error)
+ }
+
+ val driverData = GpuDriverHelper.getMetadataFromZip(driverFile)
+ val driverInList =
+ driverViewModel.driverList.value.firstOrNull { it.second == driverData }
+ if (driverInList != null) {
+ return@newInstance getString(R.string.driver_already_installed)
+ } else {
+ driverViewModel.addDriver(Pair(driverPath, driverData))
+ driverViewModel.setNewDriverInstalled(true)
+ }
+ return@newInstance Any()
+ }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
new file mode 100644
index 000000000..f8c34346a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
@@ -0,0 +1,75 @@
+// 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+
+class DriversLoadingDialogFragment : DialogFragment() {
+ private val driverViewModel: DriverViewModel by activityViewModels()
+
+ private lateinit var binding: DialogProgressBarBinding
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ binding = DialogProgressBarBinding.inflate(layoutInflater)
+ binding.progressBar.isIndeterminate = true
+
+ isCancelable = false
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.loading)
+ .setView(binding.root)
+ .create()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = binding.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.areDriversLoading.collect { checkForDismiss() }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.isDriverReady.collect { checkForDismiss() }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
+ }
+ }
+ }
+ }
+
+ private fun checkForDismiss() {
+ if (driverViewModel.isInteractionAllowed) {
+ dismiss()
+ }
+ }
+
+ companion object {
+ const val TAG = "DriversLoadingDialogFragment"
+ }
+}
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 3e6c157c7..598a9d42b 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
@@ -15,9 +15,9 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
-import android.util.Rational
import android.view.*
import android.widget.TextView
+import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat
@@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -50,10 +51,12 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.utils.*
+import java.lang.NullPointerException
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var preferences: SharedPreferences
@@ -69,6 +72,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var game: Game
private val emulationViewModel: EmulationViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
private var isInFoldableLayout = false
@@ -105,10 +109,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
null
}
}
- game = if (args.game != null) {
- args.game!!
- } else {
- intentGame ?: error("[EmulationFragment] No bootable game present!")
+
+ try {
+ game = if (args.game != null) {
+ args.game!!
+ } else {
+ intentGame!!
+ }
+ } catch (e: NullPointerException) {
+ Toast.makeText(
+ requireContext(),
+ R.string.no_game_present,
+ Toast.LENGTH_SHORT
+ ).show()
+ requireActivity().finish()
+ return
}
// So this fragment doesn't restart on configuration changes; i.e. rotation.
@@ -132,6 +147,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ if (requireActivity().isFinishing) {
+ return
+ }
+
binding.surfaceEmulation.holder.addCallback(this)
binding.showFpsText.setTextColor(Color.YELLOW)
binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
@@ -282,29 +302,43 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
}
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.isDriverReady.collect {
+ if (it && !emulationState.isRunning) {
+ if (!DirectoryInitialization.areDirectoriesReady) {
+ DirectoryInitialization.start()
+ }
+
+ updateScreenLayout()
+
+ emulationState.run(emulationActivity!!.isActivityRecreated)
+ }
+ }
+ }
+ }
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
+ if (_binding == null) {
+ return
+ }
+
+ updateScreenLayout()
if (emulationActivity?.isInPictureInPictureMode == true) {
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
}
if (EmulationMenuSettings.showOverlay) {
- binding.surfaceInputOverlay.post {
- binding.surfaceInputOverlay.visibility = View.VISIBLE
- }
+ binding.surfaceInputOverlay.visibility = View.INVISIBLE
}
} else {
if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) {
- binding.surfaceInputOverlay.post {
- binding.surfaceInputOverlay.visibility = View.VISIBLE
- }
+ binding.surfaceInputOverlay.visibility = View.VISIBLE
} else {
- binding.surfaceInputOverlay.post {
- binding.surfaceInputOverlay.visibility = View.INVISIBLE
- }
+ binding.surfaceInputOverlay.visibility = View.INVISIBLE
}
if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
@@ -316,19 +350,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
- override fun onResume() {
- super.onResume()
- if (!DirectoryInitialization.areDirectoriesReady) {
- DirectoryInitialization.start()
- }
-
- updateScreenLayout()
-
- emulationState.run(emulationActivity!!.isActivityRecreated)
- }
-
override fun onPause() {
- if (emulationState.isRunning) {
+ if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
emulationState.pause()
}
super.onPause()
@@ -394,16 +417,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
private fun updateScreenLayout() {
- binding.surfaceEmulation.setAspectRatio(
- when (IntSetting.RENDERER_ASPECT_RATIO.int) {
- 0 -> Rational(16, 9)
- 1 -> Rational(4, 3)
- 2 -> Rational(21, 9)
- 3 -> Rational(16, 10)
- 4 -> null // Stretch
- else -> Rational(16, 9)
- }
- )
+ binding.surfaceEmulation.setAspectRatio(null)
emulationActivity?.buildPictureInPictureParams()
updateOrientation()
}
@@ -693,7 +707,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private class EmulationState(private val gamePath: String) {
private var state: State
private var surface: Surface? = null
- private var runWhenSurfaceIsValid = false
init {
// Starting state is stopped.
@@ -751,8 +764,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// If the surface is set, run now. Otherwise, wait for it to get set.
if (surface != null) {
runWithValidSurface()
- } else {
- runWhenSurfaceIsValid = true
}
}
@@ -760,7 +771,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@Synchronized
fun newSurface(surface: Surface?) {
this.surface = surface
- if (runWhenSurfaceIsValid) {
+ if (this.surface != null) {
runWithValidSurface()
}
}
@@ -788,10 +799,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
private fun runWithValidSurface() {
- runWhenSurfaceIsValid = false
+ NativeLibrary.surfaceChanged(surface)
when (state) {
State.STOPPED -> {
- NativeLibrary.surfaceChanged(surface)
val emulationThread = Thread({
Log.debug("[EmulationFragment] Starting emulation thread.")
NativeLibrary.run(gamePath)
@@ -801,7 +811,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
State.PAUSED -> {
Log.debug("[EmulationFragment] Resuming emulation.")
- NativeLibrary.surfaceChanged(surface)
NativeLibrary.unpauseEmulation()
}
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 c119e69c9..fd9785075 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
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.fragments
import android.Manifest
import android.content.ActivityNotFoundException
-import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
@@ -28,7 +27,6 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.BuildConfig
import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -37,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
@@ -50,6 +49,7 @@ class HomeSettingsFragment : Fragment() {
private lateinit var mainActivity: MainActivity
private val homeViewModel: HomeViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -107,29 +107,28 @@ class HomeSettingsFragment : Fragment() {
)
add(
HomeSetting(
- R.string.install_gpu_driver,
+ R.string.gpu_driver_manager,
R.string.install_gpu_driver_description,
- R.drawable.ic_exit,
- { driverInstaller() },
+ R.drawable.ic_build,
+ {
+ binding.root.findNavController()
+ .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
+ },
{ GpuDriverHelper.supportsCustomDriverLoading() },
R.string.custom_driver_not_supported,
- R.string.custom_driver_not_supported_description
- )
- )
- add(
- HomeSetting(
- R.string.install_amiibo_keys,
- R.string.install_amiibo_keys_description,
- R.drawable.ic_nfc,
- { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }
+ R.string.custom_driver_not_supported_description,
+ driverViewModel.selectedDriverMetadata
)
)
add(
HomeSetting(
- R.string.install_game_content,
- R.string.install_game_content_description,
- R.drawable.ic_system_update_alt,
- { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
+ R.string.manage_yuzu_data,
+ R.string.manage_yuzu_data_description,
+ R.drawable.ic_install,
+ {
+ binding.root.findNavController()
+ .navigate(R.id.action_homeSettingsFragment_to_installableFragment)
+ }
)
)
add(
@@ -150,35 +149,6 @@ class HomeSettingsFragment : Fragment() {
)
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,
@@ -326,31 +296,6 @@ class HomeSettingsFragment : Fragment() {
}
}
- private fun driverInstaller() {
- // Get the driver name for the dialog message.
- var driverName = GpuDriverHelper.customDriverName
- if (driverName == null) {
- driverName = getString(R.string.system_gpu_driver)
- }
-
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(getString(R.string.select_gpu_driver_title))
- .setMessage(driverName)
- .setNegativeButton(android.R.string.cancel, null)
- .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
- GpuDriverHelper.installDefaultDriver(requireContext())
- Toast.makeText(
- requireContext(),
- R.string.select_gpu_driver_use_default,
- Toast.LENGTH_SHORT
- ).show()
- }
- .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
- mainActivity.getDriver.launch(arrayOf("application/zip"))
- }
- .show()
- }
-
private fun shareLog() {
val file = DocumentFile.fromSingleUri(
mainActivity,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
deleted file mode 100644
index f38aeea53..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
+++ /dev/null
@@ -1,213 +0,0 @@
-// 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 android.provider.DocumentsContract
-import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.documentfile.provider.DocumentFile
-import androidx.fragment.app.DialogFragment
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import java.io.BufferedOutputStream
-import java.io.File
-import java.io.FileOutputStream
-import java.io.FilenameFilter
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.features.DocumentProvider
-import org.yuzu.yuzu_emu.getPublicFilesDir
-import org.yuzu.yuzu_emu.utils.FileUtil
-
-class ImportExportSavesFragment : DialogFragment() {
- private val context = YuzuApplication.appContext
- private val savesFolder =
- "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
-
- // Get first subfolder in saves folder (should be the user folder)
- private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
- private var lastZipCreated: File? = null
-
- private lateinit var startForResultExportSave: ActivityResultLauncher<Intent>
- private lateinit var documentPicker: ActivityResultLauncher<Array<String>>
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val activity = requireActivity() as AppCompatActivity
-
- val activityResultRegistry = requireActivity().activityResultRegistry
- startForResultExportSave = activityResultRegistry.register(
- "startForResultExportSaveKey",
- ActivityResultContracts.StartActivityForResult()
- ) {
- File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
- }
- documentPicker = activityResultRegistry.register(
- "documentPickerKey",
- ActivityResultContracts.OpenDocument()
- ) {
- it?.let { uri -> importSave(uri, activity) }
- }
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- return if (savesFolderRoot == "") {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.manage_save_data)
- .setMessage(R.string.import_export_saves_no_profile)
- .setPositiveButton(android.R.string.ok, null)
- .show()
- } else {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.manage_save_data)
- .setMessage(R.string.manage_save_data_description)
- .setNegativeButton(R.string.export_saves) { _, _ ->
- exportSave()
- }
- .setPositiveButton(R.string.import_saves) { _, _ ->
- documentPicker.launch(arrayOf("application/zip"))
- }
- .setNeutralButton(android.R.string.cancel, null)
- .show()
- }
- }
-
- /**
- * Zips the save files located in the given folder path and creates a new zip file with the current date and time.
- * @return true if the zip file is successfully created, false otherwise.
- */
- private fun zipSave(): Boolean {
- try {
- val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp")
- tempFolder.mkdirs()
- val saveFolder = File(savesFolderRoot)
- val outputZipFile = File(
- tempFolder,
- "yuzu saves - ${
- LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
- }.zip"
- )
- outputZipFile.createNewFile()
- ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
- saveFolder.walkTopDown().forEach { file ->
- val zipFileName =
- file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/")
- if (zipFileName == "") {
- return@forEach
- }
- val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
- zos.putNextEntry(entry)
- if (file.isFile) {
- file.inputStream().use { fis -> fis.copyTo(zos) }
- }
- }
- }
- lastZipCreated = outputZipFile
- } catch (e: Exception) {
- return false
- }
- return true
- }
-
- /**
- * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
- */
- private fun exportSave() {
- CoroutineScope(Dispatchers.IO).launch {
- val wasZipCreated = zipSave()
- val lastZipFile = lastZipCreated
- if (!wasZipCreated || lastZipFile == null) {
- withContext(Dispatchers.Main) {
- Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show()
- }
- return@launch
- }
-
- withContext(Dispatchers.Main) {
- val file = DocumentFile.fromSingleUri(
- context,
- DocumentsContract.buildDocumentUri(
- DocumentProvider.AUTHORITY,
- "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}"
- )
- )!!
- val intent = Intent(Intent.ACTION_SEND)
- .setDataAndType(file.uri, "application/zip")
- .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- .putExtra(Intent.EXTRA_STREAM, file.uri)
- startForResultExportSave.launch(Intent.createChooser(intent, "Share save file"))
- }
- }
- }
-
- /**
- * Imports the save files contained in the zip file, and replaces any existing ones with the new save file.
- * @param zipUri The Uri of the zip file containing the save file(s) to import.
- */
- private fun importSave(zipUri: Uri, activity: AppCompatActivity) {
- val inputZip = context.contentResolver.openInputStream(zipUri)
- // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
- var validZip = false
- val savesFolder = File(savesFolderRoot)
- val cacheSaveDir = File("${context.cacheDir.path}/saves/")
- cacheSaveDir.mkdir()
-
- if (inputZip == null) {
- Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
- .show()
- return
- }
-
- val filterTitleId =
- FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
-
- try {
- CoroutineScope(Dispatchers.IO).launch {
- FileUtil.unzip(inputZip, cacheSaveDir)
- cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
- File(savesFolder, savePath).deleteRecursively()
- File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true)
- validZip = true
- }
-
- withContext(Dispatchers.Main) {
- if (!validZip) {
- MessageDialogFragment.newInstance(
- titleId = R.string.save_file_invalid_zip_structure,
- descriptionId = R.string.save_file_invalid_zip_structure_description
- ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
- return@withContext
- }
- Toast.makeText(
- context,
- context.getString(R.string.save_file_imported_success),
- Toast.LENGTH_LONG
- ).show()
- }
-
- cacheSaveDir.deleteRecursively()
- }
- } catch (e: Exception) {
- Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
- .show()
- }
- }
-
- companion object {
- const val TAG = "ImportExportSavesFragment"
- }
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
index 18bc34b9f..7e467814d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -9,8 +9,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
@@ -18,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.model.TaskViewModel
@@ -28,19 +30,25 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE)
+ val cancellable = requireArguments().getBoolean(CANCELLABLE)
binding = DialogProgressBarBinding.inflate(layoutInflater)
binding.progressBar.isIndeterminate = true
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(titleId)
.setView(binding.root)
- .create()
- dialog.setCanceledOnTouchOutside(false)
+
+ if (cancellable) {
+ dialog.setNegativeButton(android.R.string.cancel, null)
+ }
+
+ val alertDialog = dialog.create()
+ alertDialog.setCanceledOnTouchOutside(false)
if (!taskViewModel.isRunning.value) {
taskViewModel.runTask()
}
- return dialog
+ return alertDialog
}
override fun onCreateView(
@@ -53,41 +61,74 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- viewLifecycleOwner.lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- taskViewModel.isComplete.collect {
- if (it) {
- dismiss()
- when (val result = taskViewModel.result.value) {
- is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG)
- .show()
-
- is MessageDialogFragment -> result.show(
- requireActivity().supportFragmentManager,
- MessageDialogFragment.TAG
- )
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ taskViewModel.isComplete.collect {
+ if (it) {
+ dismiss()
+ when (val result = taskViewModel.result.value) {
+ is String -> Toast.makeText(
+ requireContext(),
+ result,
+ Toast.LENGTH_LONG
+ ).show()
+
+ is MessageDialogFragment -> result.show(
+ requireActivity().supportFragmentManager,
+ MessageDialogFragment.TAG
+ )
+
+ else -> {
+ // Do nothing
+ }
+ }
+ taskViewModel.clear()
+ }
+ }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ taskViewModel.cancelled.collect {
+ if (it) {
+ dialog?.setTitle(R.string.cancelling)
}
- taskViewModel.clear()
}
}
}
}
}
+ // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed.
+ // Setting the OnClickListener again after the dialog is shown overrides this behavior.
+ override fun onResume() {
+ super.onResume()
+ val alertDialog = dialog as AlertDialog
+ val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
+ negativeButton.setOnClickListener {
+ alertDialog.setTitle(getString(R.string.cancelling))
+ taskViewModel.setCancelled(true)
+ }
+ }
+
companion object {
const val TAG = "IndeterminateProgressDialogFragment"
private const val TITLE = "Title"
+ private const val CANCELLABLE = "Cancellable"
fun newInstance(
- activity: AppCompatActivity,
+ activity: FragmentActivity,
titleId: Int,
+ cancellable: Boolean = false,
task: () -> Any
): IndeterminateProgressDialogFragment {
val dialog = IndeterminateProgressDialogFragment()
val args = Bundle()
ViewModelProvider(activity)[TaskViewModel::class.java].task = task
args.putInt(TITLE, titleId)
+ args.putBoolean(CANCELLABLE, cancellable)
dialog.arguments = args
return dialog
}
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
new file mode 100644
index 000000000..ec116ab62
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -0,0 +1,138 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+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.navigation.findNavController
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.R
+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.ui.main.MainActivity
+
+class InstallableFragment : Fragment() {
+ private var _binding: FragmentInstallablesBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentInstallablesBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val mainActivity = requireActivity() as MainActivity
+
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ binding.toolbarInstallables.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
+ val installables = listOf(
+ Installable(
+ R.string.user_data,
+ R.string.user_data_description,
+ install = { mainActivity.importUserData.launch(arrayOf("application/zip")) },
+ export = { mainActivity.exportUserData.launch("export.zip") }
+ ),
+ Installable(
+ R.string.install_game_content,
+ R.string.install_game_content_description,
+ install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
+ ),
+ Installable(
+ R.string.install_firmware,
+ R.string.install_firmware_description,
+ install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
+ ),
+ if (mainActivity.savesFolderRoot != "") {
+ Installable(
+ R.string.manage_save_data,
+ R.string.import_export_saves_description,
+ install = { mainActivity.importSaves.launch(arrayOf("application/zip")) },
+ export = { mainActivity.exportSave() }
+ )
+ } else {
+ Installable(
+ R.string.manage_save_data,
+ R.string.import_export_saves_description,
+ install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }
+ )
+ },
+ Installable(
+ R.string.install_prod_keys,
+ R.string.install_prod_keys_description,
+ install = { mainActivity.getProdKey.launch(arrayOf("*/*")) }
+ ),
+ Installable(
+ R.string.install_amiibo_keys,
+ R.string.install_amiibo_keys_description,
+ install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }
+ )
+ )
+
+ binding.listInstallables.apply {
+ layoutManager = GridLayoutManager(
+ requireContext(),
+ resources.getInteger(R.integer.grid_columns)
+ )
+ adapter = InstallableAdapter(installables)
+ }
+
+ setInsets()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.root
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpAppBar = binding.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams
+ mlpAppBar.leftMargin = leftInsets
+ mlpAppBar.rightMargin = rightInsets
+ binding.toolbarInstallables.layoutParams = mlpAppBar
+
+ val mlpScrollAbout =
+ binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams
+ mlpScrollAbout.leftMargin = leftInsets
+ mlpScrollAbout.rightMargin = rightInsets
+ binding.listInstallables.layoutParams = mlpScrollAbout
+
+ binding.listInstallables.updatePadding(bottom = barInsets.bottom)
+
+ windowInsets
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
index 7d1c2c8dd..541b22f47 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
@@ -4,14 +4,21 @@
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
+import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.model.MessageDialogViewModel
class MessageDialogFragment : DialogFragment() {
+ private val messageDialogViewModel: MessageDialogViewModel by activityViewModels()
+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE_ID)
val titleString = requireArguments().getString(TITLE_STRING)!!
@@ -37,6 +44,12 @@ class MessageDialogFragment : DialogFragment() {
return dialog.show()
}
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ messageDialogViewModel.dismissAction.invoke()
+ messageDialogViewModel.clear()
+ }
+
private fun openLink(link: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
startActivity(intent)
@@ -52,11 +65,13 @@ class MessageDialogFragment : DialogFragment() {
private const val HELP_LINK = "Link"
fun newInstance(
+ activity: FragmentActivity,
titleId: Int = 0,
titleString: String = "",
descriptionId: Int = 0,
descriptionString: String = "",
- helpLinkId: Int = 0
+ helpLinkId: Int = 0,
+ dismissAction: () -> Unit = {}
): MessageDialogFragment {
val dialog = MessageDialogFragment()
val bundle = Bundle()
@@ -67,6 +82,8 @@ class MessageDialogFragment : DialogFragment() {
putString(DESCRIPTION_STRING, descriptionString)
putInt(HELP_LINK, helpLinkId)
}
+ ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
+ dismissAction
dialog.arguments = bundle
return dialog
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
index fbb2f6e18..c66bb635a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -295,8 +295,10 @@ class SetupFragment : Fragment() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
- outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
- outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
+ if (_binding != null) {
+ outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
+ outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
+ }
outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
}
@@ -353,11 +355,15 @@ class SetupFragment : Fragment() {
}
fun pageForward() {
- binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
+ if (_binding != null) {
+ binding.viewPager2.currentItem += 1
+ }
}
fun pageBackward() {
- binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
+ if (_binding != null) {
+ binding.viewPager2.currentItem -= 1
+ }
}
fun setPageWarned(page: Int) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
new file mode 100644
index 000000000..62945ad65
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
+import java.io.BufferedOutputStream
+import java.io.File
+
+class DriverViewModel : ViewModel() {
+ private val _areDriversLoading = MutableStateFlow(false)
+ val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
+
+ private val _isDriverReady = MutableStateFlow(true)
+ val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
+
+ private val _isDeletingDrivers = MutableStateFlow(false)
+ val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
+
+ private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>())
+ val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
+
+ var previouslySelectedDriver = 0
+ var selectedDriver = -1
+
+ private val _selectedDriverMetadata =
+ MutableStateFlow(
+ GpuDriverHelper.customDriverData.name
+ ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+ )
+ val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
+
+ private val _newDriverInstalled = MutableStateFlow(false)
+ val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
+
+ val driversToDelete = mutableListOf<String>()
+
+ val isInteractionAllowed
+ get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
+
+ init {
+ _areDriversLoading.value = true
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ val drivers = GpuDriverHelper.getDrivers()
+ val currentDriverMetadata = GpuDriverHelper.customDriverData
+ for (i in drivers.indices) {
+ if (drivers[i].second == currentDriverMetadata) {
+ setSelectedDriverIndex(i)
+ break
+ }
+ }
+
+ // If a user had installed a driver before the manager was implemented, this zips
+ // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
+ // be indexed and exported as expected.
+ if (selectedDriver == -1) {
+ val driverToSave =
+ File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
+ driverToSave.createNewFile()
+ FileUtil.zipFromInternalStorage(
+ File(GpuDriverHelper.driverInstallationPath!!),
+ GpuDriverHelper.driverInstallationPath!!,
+ BufferedOutputStream(driverToSave.outputStream())
+ )
+ drivers.add(Pair(driverToSave.path, currentDriverMetadata))
+ setSelectedDriverIndex(drivers.size - 1)
+ }
+
+ _driverList.value = drivers
+ _areDriversLoading.value = false
+ }
+ }
+ }
+
+ fun setSelectedDriverIndex(value: Int) {
+ if (selectedDriver != -1) {
+ previouslySelectedDriver = selectedDriver
+ }
+ selectedDriver = value
+ }
+
+ fun setNewDriverInstalled(value: Boolean) {
+ _newDriverInstalled.value = value
+ }
+
+ fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
+ val driverIndex = _driverList.value.indexOfFirst { it == driverData }
+ if (driverIndex == -1) {
+ setSelectedDriverIndex(_driverList.value.size)
+ _driverList.value.add(driverData)
+ _selectedDriverMetadata.value = driverData.second.name
+ ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+ } else {
+ setSelectedDriverIndex(driverIndex)
+ }
+ }
+
+ fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) {
+ _driverList.value.remove(driverData)
+ }
+
+ fun onCloseDriverManager() {
+ _isDeletingDrivers.value = true
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ driversToDelete.forEach {
+ val driver = File(it)
+ if (driver.exists()) {
+ driver.delete()
+ }
+ }
+ driversToDelete.clear()
+ _isDeletingDrivers.value = false
+ }
+ }
+
+ if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
+ return
+ }
+
+ _isDriverReady.value = false
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ if (selectedDriver == 0) {
+ GpuDriverHelper.installDefaultDriver()
+ setDriverReady()
+ return@withContext
+ }
+
+ val driverToInstall = File(driverList.value[selectedDriver].first)
+ if (driverToInstall.exists()) {
+ GpuDriverHelper.installCustomDriver(driverToInstall)
+ } else {
+ GpuDriverHelper.installDefaultDriver()
+ }
+ setDriverReady()
+ }
+ }
+ }
+
+ private fun setDriverReady() {
+ _isDriverReady.value = true
+ _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
+ ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt
new file mode 100644
index 000000000..36a7c97b8
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.annotation.StringRes
+
+data class Installable(
+ @StringRes val titleId: Int,
+ @StringRes val descriptionId: Int,
+ val install: (() -> Unit)? = null,
+ val export: (() -> Unit)? = null
+)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
new file mode 100644
index 000000000..36ffd08d2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.ViewModel
+
+class MessageDialogViewModel : ViewModel() {
+ var dismissAction: () -> Unit = {}
+
+ fun clear() {
+ dismissAction = {}
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
index 531c2aaf0..16a794dee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -20,12 +20,20 @@ class TaskViewModel : ViewModel() {
val isRunning: StateFlow<Boolean> get() = _isRunning
private val _isRunning = MutableStateFlow(false)
+ val cancelled: StateFlow<Boolean> get() = _cancelled
+ private val _cancelled = MutableStateFlow(false)
+
lateinit var task: () -> Any
fun clear() {
_result.value = Any()
_isComplete.value = false
_isRunning.value = false
+ _cancelled.value = false
+ }
+
+ fun setCancelled(value: Boolean) {
+ _cancelled.value = value
}
fun runTask() {
@@ -42,3 +50,9 @@ class TaskViewModel : ViewModel() {
}
}
}
+
+enum class TaskState {
+ Completed,
+ Failed,
+ Cancelled
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
index c055c2e35..a13faf3c7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -352,7 +352,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
}
private fun addOverlayControls(layout: String) {
- val windowSize = getSafeScreenSize(context)
+ val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight))
if (preferences.getBoolean(Settings.PREF_BUTTON_A, true)) {
overlayButtons.add(
initializeOverlayButton(
@@ -593,7 +593,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
}
private fun saveControlPosition(prefId: String, x: Int, y: Int, layout: String) {
- val windowSize = getSafeScreenSize(context)
+ val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight))
val min = windowSize.first
val max = windowSize.second
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
@@ -968,14 +968,17 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
* @return A pair of points, the first being the top left corner of the safe area,
* the second being the bottom right corner of the safe area
*/
- private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
+ private fun getSafeScreenSize(
+ context: Context,
+ screenSize: Pair<Int, Int>
+ ): Pair<Point, Point> {
// Get screen size
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(context as Activity)
- var maxY = windowMetrics.bounds.height().toFloat()
- var maxX = windowMetrics.bounds.width().toFloat()
- var minY = 0
+ var maxX = screenSize.first.toFloat()
+ var maxY = screenSize.second.toFloat()
var minX = 0
+ var minY = 0
// If we have API access, calculate the safe area to draw the overlay
var cutoutLeft = 0
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 b6b6c6c17..233aa4101 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
@@ -6,6 +6,7 @@ package org.yuzu.yuzu_emu.ui.main
import android.content.Intent
import android.net.Uri
import android.os.Bundle
+import android.provider.DocumentsContract
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
@@ -19,6 +20,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -27,11 +29,10 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationBarView
+import kotlinx.coroutines.CoroutineScope
import java.io.File
import java.io.FilenameFilter
-import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -40,22 +41,40 @@ import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
-import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
+import org.yuzu.yuzu_emu.getPublicFilesDir
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.TaskState
+import org.yuzu.yuzu_emu.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.*
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
class MainActivity : AppCompatActivity(), ThemeProvider {
private lateinit var binding: ActivityMainBinding
private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels()
+ private val taskViewModel: TaskViewModel by viewModels()
override var themeId: Int = 0
+ private val savesFolder
+ get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
+
+ // Get first subfolder in saves folder (should be the user folder)
+ val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
+ private var lastZipCreated: File? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@@ -307,6 +326,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
fun processKey(result: Uri): Boolean {
if (FileUtil.getExtension(result) != "keys") {
MessageDialogFragment.newInstance(
+ this,
titleId = R.string.reading_keys_failure,
descriptionId = R.string.install_prod_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG)
@@ -320,11 +340,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
if (FileUtil.copyUriToInternalStorage(
- applicationContext,
result,
dstPath,
"prod.keys"
- )
+ ) != null
) {
if (NativeLibrary.reloadKeys()) {
Toast.makeText(
@@ -336,6 +355,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return true
} else {
MessageDialogFragment.newInstance(
+ this,
titleId = R.string.invalid_keys_error,
descriptionId = R.string.install_keys_failure_description,
helpLinkId = R.string.dumping_keys_quickstart_link
@@ -371,11 +391,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val task: () -> Any = {
var messageToShow: Any
try {
- FileUtil.unzip(inputZip, cacheFirmwareDir)
+ FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
MessageDialogFragment.newInstance(
+ this,
titleId = R.string.firmware_installed_failure,
descriptionId = R.string.firmware_installed_failure_description
)
@@ -395,7 +416,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
IndeterminateProgressDialogFragment.newInstance(
this,
R.string.firmware_installing,
- task
+ task = task
).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
@@ -407,6 +428,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (FileUtil.getExtension(result) != "bin") {
MessageDialogFragment.newInstance(
+ this,
titleId = R.string.reading_keys_failure,
descriptionId = R.string.install_amiibo_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG)
@@ -420,11 +442,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
if (FileUtil.copyUriToInternalStorage(
- applicationContext,
result,
dstPath,
"key_retail.bin"
- )
+ ) != null
) {
if (NativeLibrary.reloadKeys()) {
Toast.makeText(
@@ -434,6 +455,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
).show()
} else {
MessageDialogFragment.newInstance(
+ this,
titleId = R.string.invalid_keys_error,
descriptionId = R.string.install_keys_failure_description,
helpLinkId = R.string.dumping_keys_quickstart_link
@@ -442,66 +464,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}
- val getDriver =
- registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
- if (result == null) {
- return@registerForActivityResult
- }
-
- val takeFlags =
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
- contentResolver.takePersistableUriPermission(
- result,
- takeFlags
- )
-
- val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
- progressBinding.progressBar.isIndeterminate = true
- val installationDialog = MaterialAlertDialogBuilder(this)
- .setTitle(R.string.installing_driver)
- .setView(progressBinding.root)
- .show()
-
- lifecycleScope.launch {
- withContext(Dispatchers.IO) {
- // Ignore file exceptions when a user selects an invalid zip
- try {
- GpuDriverHelper.installCustomDriver(applicationContext, result)
- } catch (_: IOException) {
- }
-
- withContext(Dispatchers.Main) {
- installationDialog.dismiss()
-
- val driverName = GpuDriverHelper.customDriverName
- if (driverName != null) {
- Toast.makeText(
- applicationContext,
- getString(
- R.string.select_gpu_driver_install_success,
- driverName
- ),
- Toast.LENGTH_SHORT
- ).show()
- } else {
- Toast.makeText(
- applicationContext,
- R.string.select_gpu_driver_error,
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
- }
- }
-
val installGameUpdate = registerForActivityResult(
ActivityResultContracts.OpenMultipleDocuments()
) { documents: List<Uri> ->
if (documents.isNotEmpty()) {
IndeterminateProgressDialogFragment.newInstance(
this@MainActivity,
- R.string.install_game_content
+ R.string.installing_game_content
) {
var installSuccess = 0
var installOverwrite = 0
@@ -509,7 +478,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
var errorExtension = 0
var errorOther = 0
documents.forEach {
- when (NativeLibrary.installFileToNand(it.toString())) {
+ when (
+ NativeLibrary.installFileToNand(
+ it.toString(),
+ FileUtil.getExtension(it)
+ )
+ ) {
NativeLibrary.InstallFileToNandResult.Success -> {
installSuccess += 1
}
@@ -583,12 +557,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
installResult.append(separator)
}
return@newInstance MessageDialogFragment.newInstance(
+ this,
titleId = R.string.install_game_content_failure,
descriptionString = installResult.toString().trim(),
helpLinkId = R.string.install_game_content_help_link
)
} else {
return@newInstance MessageDialogFragment.newInstance(
+ this,
titleId = R.string.install_game_content_success,
descriptionString = installResult.toString().trim()
)
@@ -596,4 +572,228 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
}
+
+ val exportUserData = registerForActivityResult(
+ ActivityResultContracts.CreateDocument("application/zip")
+ ) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ this,
+ R.string.exporting_user_data,
+ true
+ ) {
+ val zipResult = FileUtil.zipFromInternalStorage(
+ File(DirectoryInitialization.userDirectory!!),
+ DirectoryInitialization.userDirectory!!,
+ BufferedOutputStream(contentResolver.openOutputStream(result)),
+ taskViewModel.cancelled
+ )
+ return@newInstance when (zipResult) {
+ TaskState.Completed -> getString(R.string.user_data_export_success)
+ TaskState.Failed -> R.string.export_failed
+ TaskState.Cancelled -> R.string.user_data_export_cancelled
+ }
+ }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+
+ val importUserData =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ this,
+ R.string.importing_user_data
+ ) {
+ val checkStream =
+ ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
+ var isYuzuBackup = false
+ checkStream.use { stream ->
+ var ze: ZipEntry? = null
+ while (stream.nextEntry?.also { ze = it } != null) {
+ val itemName = ze!!.name.trim()
+ if (itemName == "/config/config.ini" || itemName == "config/config.ini") {
+ isYuzuBackup = true
+ return@use
+ }
+ }
+ }
+ if (!isYuzuBackup) {
+ return@newInstance MessageDialogFragment.newInstance(
+ this,
+ titleId = R.string.invalid_yuzu_backup,
+ descriptionId = R.string.user_data_import_failed_description
+ )
+ }
+
+ // Clear existing user data
+ File(DirectoryInitialization.userDirectory!!).deleteRecursively()
+
+ // Copy archive to internal storage
+ try {
+ FileUtil.unzipToInternalStorage(
+ BufferedInputStream(contentResolver.openInputStream(result)),
+ File(DirectoryInitialization.userDirectory!!)
+ )
+ } catch (e: Exception) {
+ return@newInstance MessageDialogFragment.newInstance(
+ this,
+ titleId = R.string.import_failed,
+ descriptionId = R.string.user_data_import_failed_description
+ )
+ }
+
+ // Reinitialize relevant data
+ NativeLibrary.initializeEmulation()
+ gamesViewModel.reloadGames(false)
+
+ return@newInstance getString(R.string.user_data_import_success)
+ }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+
+ /**
+ * Zips the save files located in the given folder path and creates a new zip file with the current date and time.
+ * @return true if the zip file is successfully created, false otherwise.
+ */
+ private fun zipSave(): Boolean {
+ try {
+ val tempFolder = File(getPublicFilesDir().canonicalPath, "temp")
+ tempFolder.mkdirs()
+ val saveFolder = File(savesFolderRoot)
+ val outputZipFile = File(
+ tempFolder,
+ "yuzu saves - ${
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
+ }.zip"
+ )
+ outputZipFile.createNewFile()
+ val result = FileUtil.zipFromInternalStorage(
+ saveFolder,
+ savesFolderRoot,
+ BufferedOutputStream(FileOutputStream(outputZipFile))
+ )
+ if (result == TaskState.Failed) {
+ return false
+ }
+ lastZipCreated = outputZipFile
+ } catch (e: Exception) {
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
+ */
+ fun exportSave() {
+ CoroutineScope(Dispatchers.IO).launch {
+ val wasZipCreated = zipSave()
+ val lastZipFile = lastZipCreated
+ if (!wasZipCreated || lastZipFile == null) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ this@MainActivity,
+ getString(R.string.export_save_failed),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ return@launch
+ }
+
+ withContext(Dispatchers.Main) {
+ val file = DocumentFile.fromSingleUri(
+ this@MainActivity,
+ DocumentsContract.buildDocumentUri(
+ DocumentProvider.AUTHORITY,
+ "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}"
+ )
+ )!!
+ val intent = Intent(Intent.ACTION_SEND)
+ .setDataAndType(file.uri, "application/zip")
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .putExtra(Intent.EXTRA_STREAM, file.uri)
+ startForResultExportSave.launch(
+ Intent.createChooser(
+ intent,
+ getString(R.string.share_save_file)
+ )
+ )
+ }
+ }
+ }
+
+ private val startForResultExportSave =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
+ File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
+ }
+
+ val importSaves =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ NativeLibrary.initializeEmptyUserDirectory()
+
+ val inputZip = contentResolver.openInputStream(result)
+ // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
+ var validZip = false
+ val savesFolder = File(savesFolderRoot)
+ val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
+ cacheSaveDir.mkdir()
+
+ if (inputZip == null) {
+ Toast.makeText(
+ applicationContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ return@registerForActivityResult
+ }
+
+ val filterTitleId =
+ FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
+
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+ cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
+ File(savesFolder, savePath).deleteRecursively()
+ File(cacheSaveDir, savePath).copyRecursively(
+ File(savesFolder, savePath),
+ true
+ )
+ validZip = true
+ }
+
+ withContext(Dispatchers.Main) {
+ if (!validZip) {
+ MessageDialogFragment.newInstance(
+ this@MainActivity,
+ titleId = R.string.save_file_invalid_zip_structure,
+ descriptionId = R.string.save_file_invalid_zip_structure_description
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ return@withContext
+ }
+ Toast.makeText(
+ applicationContext,
+ getString(R.string.save_file_imported_success),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
+ cacheSaveDir.deleteRecursively()
+ }
+ } catch (e: Exception) {
+ Toast.makeText(
+ applicationContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
index cf226ad94..eafcf9e42 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
@@ -7,7 +7,6 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import java.io.File
import java.util.*
-import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
class DocumentsTree {
@@ -22,7 +21,7 @@ class DocumentsTree {
fun openContentUri(filepath: String, openMode: String?): Int {
val node = resolvePath(filepath) ?: return -1
- return FileUtil.openContentUri(YuzuApplication.appContext, node.uri.toString(), openMode)
+ return FileUtil.openContentUri(node.uri.toString(), openMode)
}
fun getFileSize(filepath: String): Long {
@@ -30,7 +29,7 @@ class DocumentsTree {
return if (node == null || node.isDirectory) {
0
} else {
- FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString())
+ FileUtil.getFileSize(node.uri.toString())
}
}
@@ -67,7 +66,7 @@ class DocumentsTree {
* @param parent parent node of this level
*/
private fun structTree(parent: DocumentsNode) {
- val documents = FileUtil.listFiles(YuzuApplication.appContext, parent.uri!!)
+ val documents = FileUtil.listFiles(parent.uri!!)
for (document in documents) {
val node = DocumentsNode(document)
node.parent = parent
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 142af5f26..5ee74a52c 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
@@ -3,14 +3,13 @@
package org.yuzu.yuzu_emu.utils
-import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
+import kotlinx.coroutines.flow.StateFlow
import java.io.BufferedInputStream
import java.io.File
-import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.URLDecoder
@@ -18,6 +17,11 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
+import org.yuzu.yuzu_emu.model.TaskState
+import java.io.BufferedOutputStream
+import java.lang.NullPointerException
+import java.nio.charset.StandardCharsets
+import java.util.zip.ZipOutputStream
object FileUtil {
const val PATH_TREE = "tree"
@@ -25,6 +29,8 @@ object FileUtil {
const val APPLICATION_OCTET_STREAM = "application/octet-stream"
const val TEXT_PLAIN = "text/plain"
+ private val context get() = YuzuApplication.appContext
+
/**
* Create a file from directory with filename.
* @param context Application context
@@ -32,11 +38,11 @@ object FileUtil {
* @param filename file display name.
* @return boolean
*/
- fun createFile(context: Context?, directory: String?, filename: String): DocumentFile? {
+ fun createFile(directory: String?, filename: String): DocumentFile? {
var decodedFilename = filename
try {
val directoryUri = Uri.parse(directory)
- val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+ val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
decodedFilename = URLDecoder.decode(decodedFilename, DECODE_METHOD)
var mimeType = APPLICATION_OCTET_STREAM
if (decodedFilename.endsWith(".txt")) {
@@ -52,16 +58,15 @@ object FileUtil {
/**
* Create a directory from directory with filename.
- * @param context Application context
* @param directory parent path for directory.
* @param directoryName directory display name.
* @return boolean
*/
- fun createDir(context: Context?, directory: String?, directoryName: String?): DocumentFile? {
+ fun createDir(directory: String?, directoryName: String?): DocumentFile? {
var decodedDirectoryName = directoryName
try {
val directoryUri = Uri.parse(directory)
- val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+ val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
decodedDirectoryName = URLDecoder.decode(decodedDirectoryName, DECODE_METHOD)
val isExist = parent.findFile(decodedDirectoryName)
return isExist ?: parent.createDirectory(decodedDirectoryName)
@@ -73,13 +78,12 @@ object FileUtil {
/**
* Open content uri and return file descriptor to JNI.
- * @param context Application context
* @param path Native content uri path
* @param openMode will be one of "r", "r", "rw", "wa", "rwa"
* @return file descriptor
*/
@JvmStatic
- fun openContentUri(context: Context, path: String, openMode: String?): Int {
+ fun openContentUri(path: String, openMode: String?): Int {
try {
val uri = Uri.parse(path)
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, openMode!!)
@@ -99,11 +103,10 @@ object FileUtil {
/**
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
* This function will be faster than DoucmentFile.listFiles
- * @param context Application context
* @param uri Directory uri.
* @return CheapDocument lists.
*/
- fun listFiles(context: Context, uri: Uri): Array<MinimalDocumentFile> {
+ fun listFiles(uri: Uri): Array<MinimalDocumentFile> {
val resolver = context.contentResolver
val columns = arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
@@ -141,7 +144,7 @@ object FileUtil {
* @param path Native content uri path
* @return bool
*/
- fun exists(context: Context, path: String?): Boolean {
+ fun exists(path: String?): Boolean {
var c: Cursor? = null
try {
val mUri = Uri.parse(path)
@@ -161,7 +164,7 @@ object FileUtil {
* @param path content uri path
* @return bool
*/
- fun isDirectory(context: Context, path: String): Boolean {
+ fun isDirectory(path: String): Boolean {
val resolver = context.contentResolver
val columns = arrayOf(
DocumentsContract.Document.COLUMN_MIME_TYPE
@@ -206,10 +209,10 @@ object FileUtil {
return filename
}
- fun getFilesName(context: Context, path: String): Array<String> {
+ fun getFilesName(path: String): Array<String> {
val uri = Uri.parse(path)
val files: MutableList<String> = ArrayList()
- for (file in listFiles(context, uri)) {
+ for (file in listFiles(uri)) {
files.add(file.filename)
}
return files.toTypedArray()
@@ -221,7 +224,7 @@ object FileUtil {
* @return long file size
*/
@JvmStatic
- fun getFileSize(context: Context, path: String): Long {
+ fun getFileSize(path: String): Long {
val resolver = context.contentResolver
val columns = arrayOf(
DocumentsContract.Document.COLUMN_SIZE
@@ -241,71 +244,100 @@ object FileUtil {
return size
}
+ /**
+ * Creates an input stream with a given [Uri] and copies its data to the given path. This will
+ * overwrite any pre-existing files.
+ *
+ * @param sourceUri The [Uri] to copy data from
+ * @param destinationParentPath Destination directory
+ * @param destinationFilename Optionally renames the file once copied
+ */
fun copyUriToInternalStorage(
- context: Context,
- sourceUri: Uri?,
+ sourceUri: Uri,
destinationParentPath: String,
- destinationFilename: String
- ): Boolean {
- var input: InputStream? = null
- var output: FileOutputStream? = null
+ destinationFilename: String = ""
+ ): File? =
try {
- input = context.contentResolver.openInputStream(sourceUri!!)
- output = FileOutputStream("$destinationParentPath/$destinationFilename")
- val buffer = ByteArray(1024)
- var len: Int
- while (input!!.read(buffer).also { len = it } != -1) {
- output.write(buffer, 0, len)
- }
- output.flush()
- return true
- } catch (e: Exception) {
- Log.error("[FileUtil]: Cannot copy file, error: " + e.message)
- } finally {
- if (input != null) {
- try {
- input.close()
- } catch (e: IOException) {
- Log.error("[FileUtil]: Cannot close input file, error: " + e.message)
- }
+ val fileName =
+ if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename"
+ val inputStream = context.contentResolver.openInputStream(sourceUri)!!
+
+ val destinationFile = File("$destinationParentPath$fileName")
+ if (destinationFile.exists()) {
+ destinationFile.delete()
}
- if (output != null) {
- try {
- output.close()
- } catch (e: IOException) {
- Log.error("[FileUtil]: Cannot close output file, error: " + e.message)
- }
+
+ destinationFile.outputStream().use { fos ->
+ inputStream.use { it.copyTo(fos) }
}
+ destinationFile
+ } catch (e: IOException) {
+ null
+ } catch (e: NullPointerException) {
+ null
}
- return false
- }
/**
* Extracts the given zip file into the given directory.
- * @exception IOException if the file was being created outside of the target directory
*/
@Throws(SecurityException::class)
- fun unzip(zipStream: InputStream, destDir: File): Boolean {
- ZipInputStream(BufferedInputStream(zipStream)).use { zis ->
+ fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
+ ZipInputStream(zipStream).use { zis ->
var entry: ZipEntry? = zis.nextEntry
while (entry != null) {
- val entryName = entry.name
- val entryFile = File(destDir, entryName)
- if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
- throw SecurityException("Entry is outside of the target dir: " + entryFile.name)
+ val newFile = File(destDir, entry.name)
+ val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
+
+ if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
+ throw SecurityException("Zip file attempted path traversal! ${entry.name}")
+ }
+
+ if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
+ throw IOException("Failed to create directory $destinationDirectory")
}
- if (entry.isDirectory) {
- entryFile.mkdirs()
- } else {
- entryFile.parentFile?.mkdirs()
- entryFile.createNewFile()
- entryFile.outputStream().use { fos -> zis.copyTo(fos) }
+
+ if (!entry.isDirectory) {
+ newFile.outputStream().use { fos -> zis.copyTo(fos) }
}
entry = zis.nextEntry
}
}
+ }
- return true
+ /**
+ * Creates a zip file from a directory within internal storage
+ * @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
+ */
+ fun zipFromInternalStorage(
+ inputFile: File,
+ rootDir: String,
+ outputStream: BufferedOutputStream,
+ cancelled: StateFlow<Boolean>? = null
+ ): TaskState {
+ try {
+ ZipOutputStream(outputStream).use { zos ->
+ inputFile.walkTopDown().forEach { file ->
+ if (cancelled?.value == true) {
+ return TaskState.Cancelled
+ }
+
+ if (!file.isDirectory) {
+ val entryName =
+ file.absolutePath.removePrefix(rootDir).removePrefix("/")
+ val entry = ZipEntry(entryName)
+ zos.putNextEntry(entry)
+ if (file.isFile) {
+ file.inputStream().use { fis -> fis.copyTo(zos) }
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ return TaskState.Failed
+ }
+ return TaskState.Completed
}
fun isRootTreeUri(uri: Uri): Boolean {
@@ -329,4 +361,12 @@ object FileUtil {
return fileName.substring(fileName.lastIndexOf(".") + 1)
.lowercase()
}
+
+ @Throws(IOException::class)
+ fun getStringFromFile(file: File): String =
+ String(file.readBytes(), StandardCharsets.UTF_8)
+
+ @Throws(IOException::class)
+ fun getStringFromInputStream(stream: InputStream): String =
+ String(stream.readBytes(), StandardCharsets.UTF_8)
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index e0ee29c9b..9001ca9ab 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -30,7 +30,7 @@ object GameHelper {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
- addGamesRecursive(games, FileUtil.listFiles(context, gamesUri), 3)
+ addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
// Cache list of games found on disk
val serializedGames = mutableSetOf<String>()
@@ -58,7 +58,7 @@ object GameHelper {
if (it.isDirectory) {
addGamesRecursive(
games,
- FileUtil.listFiles(YuzuApplication.appContext, it.uri),
+ FileUtil.listFiles(it.uri),
depth - 1
)
} else {
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 1d4695a2a..f6882ce6c 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
@@ -3,64 +3,33 @@
package org.yuzu.yuzu_emu.utils
-import android.content.Context
import android.net.Uri
+import android.os.Build
import java.io.BufferedInputStream
import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
import java.io.IOException
-import java.util.zip.ZipInputStream
import org.yuzu.yuzu_emu.NativeLibrary
-import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage
+import org.yuzu.yuzu_emu.YuzuApplication
+import java.util.zip.ZipException
+import java.util.zip.ZipFile
object GpuDriverHelper {
private const val META_JSON_FILENAME = "meta.json"
- private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip"
private var fileRedirectionPath: String? = null
- private var driverInstallationPath: String? = null
+ var driverInstallationPath: String? = null
private var hookLibPath: String? = null
- @Throws(IOException::class)
- private fun unzip(zipFilePath: String, destDir: String) {
- val dir = File(destDir)
-
- // Create output directory if it doesn't exist
- if (!dir.exists()) dir.mkdirs()
-
- // Unpack the files.
- val inputStream = FileInputStream(zipFilePath)
- val zis = ZipInputStream(BufferedInputStream(inputStream))
- val buffer = ByteArray(1024)
- var ze = zis.nextEntry
- while (ze != null) {
- val newFile = File(destDir, ze.name)
- val canonicalPath = newFile.canonicalPath
- if (!canonicalPath.startsWith(destDir + ze.name)) {
- throw SecurityException("Zip file attempted path traversal! " + ze.name)
- }
-
- newFile.parentFile!!.mkdirs()
- val fos = FileOutputStream(newFile)
- var len: Int
- while (zis.read(buffer).also { len = it } > 0) {
- fos.write(buffer, 0, len)
- }
- fos.close()
- zis.closeEntry()
- ze = zis.nextEntry
- }
- zis.closeEntry()
- }
+ val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/"
- fun initializeDriverParameters(context: Context) {
+ fun initializeDriverParameters() {
try {
// Initialize the file redirection directory.
- fileRedirectionPath =
- context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
+ fileRedirectionPath = YuzuApplication.appContext
+ .getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
// Initialize the driver installation directory.
- driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/"
+ driverInstallationPath = YuzuApplication.appContext
+ .filesDir.canonicalPath + "/gpu_driver/"
} catch (e: IOException) {
throw RuntimeException(e)
}
@@ -69,68 +38,169 @@ object GpuDriverHelper {
initializeDirectories()
// Initialize hook libraries directory.
- hookLibPath = context.applicationInfo.nativeLibraryDir + "/"
+ hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/"
// Initialize GPU driver.
NativeLibrary.initializeGpuDriver(
hookLibPath,
driverInstallationPath,
- customDriverLibraryName,
+ customDriverData.libraryName,
fileRedirectionPath
)
}
- fun installDefaultDriver(context: Context) {
+ fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> {
+ val driverZips = File(driverStoragePath).listFiles()
+ val drivers: MutableList<Pair<String, GpuDriverMetadata>> =
+ driverZips
+ ?.mapNotNull {
+ val metadata = getMetadataFromZip(it)
+ metadata.name?.let { _ -> Pair(it.path, metadata) }
+ }
+ ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
+ ?.distinct()
+ ?.toMutableList() ?: mutableListOf()
+
+ // TODO: Get system driver information
+ drivers.add(0, Pair("", GpuDriverMetadata()))
+ return drivers
+ }
+
+ fun installDefaultDriver() {
// Removing the installed driver will result in the backend using the default system driver.
- val driverInstallationDir = File(driverInstallationPath!!)
- deleteRecursive(driverInstallationDir)
- initializeDriverParameters(context)
+ File(driverInstallationPath!!).deleteRecursively()
+ initializeDriverParameters()
+ }
+
+ fun copyDriverToInternalStorage(driverUri: Uri): Boolean {
+ // Ensure we have directories.
+ initializeDirectories()
+
+ // Copy the zip file URI to user data
+ val copiedFile =
+ FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
+
+ // Validate driver
+ val metadata = getMetadataFromZip(copiedFile)
+ if (metadata.name == null) {
+ copiedFile.delete()
+ return false
+ }
+
+ if (metadata.minApi > Build.VERSION.SDK_INT) {
+ copiedFile.delete()
+ return false
+ }
+ return true
}
- fun installCustomDriver(context: Context, driverPathUri: Uri?) {
+ /**
+ * Copies driver zip into user data directory so that it can be exported along with
+ * other user data and also unzipped into the installation directory
+ */
+ fun installCustomDriver(driverUri: Uri): Boolean {
// Revert to system default in the event the specified driver is bad.
- installDefaultDriver(context)
+ installDefaultDriver()
// Ensure we have directories.
initializeDirectories()
- // Copy the zip file URI into our private storage.
- copyUriToInternalStorage(
- context,
- driverPathUri,
- driverInstallationPath!!,
- DRIVER_INTERNAL_FILENAME
- )
+ // Copy the zip file URI to user data
+ val copiedFile =
+ FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
+
+ // Validate driver
+ val metadata = getMetadataFromZip(copiedFile)
+ if (metadata.name == null) {
+ copiedFile.delete()
+ return false
+ }
+
+ if (metadata.minApi > Build.VERSION.SDK_INT) {
+ copiedFile.delete()
+ return false
+ }
// Unzip the driver.
try {
- unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!)
+ FileUtil.unzipToInternalStorage(
+ BufferedInputStream(copiedFile.inputStream()),
+ File(driverInstallationPath!!)
+ )
} catch (e: SecurityException) {
- return
+ return false
}
// Initialize the driver parameters.
- initializeDriverParameters(context)
+ initializeDriverParameters()
+
+ return true
}
- external fun supportsCustomDriverLoading(): Boolean
+ /**
+ * Unzips driver into installation directory
+ */
+ fun installCustomDriver(driver: File): Boolean {
+ // Revert to system default in the event the specified driver is bad.
+ installDefaultDriver()
- // Parse the custom driver metadata to retrieve the name.
- val customDriverName: String?
- get() {
- val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
- return metadata.name
+ // Ensure we have directories.
+ initializeDirectories()
+
+ // Validate driver
+ val metadata = getMetadataFromZip(driver)
+ if (metadata.name == null) {
+ driver.delete()
+ return false
}
- // Parse the custom driver metadata to retrieve the library name.
- private val customDriverLibraryName: String?
- get() {
- // Parse the custom driver metadata to retrieve the library name.
- val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
- return metadata.libraryName
+ // Unzip the driver to the private installation directory
+ try {
+ FileUtil.unzipToInternalStorage(
+ BufferedInputStream(driver.inputStream()),
+ File(driverInstallationPath!!)
+ )
+ } catch (e: SecurityException) {
+ return false
}
- private fun initializeDirectories() {
+ // Initialize the driver parameters.
+ initializeDriverParameters()
+
+ return true
+ }
+
+ /**
+ * Takes in a zip file and reads the meta.json file for presentation to the UI
+ *
+ * @param driver Zip containing driver and meta.json file
+ * @return A non-null [GpuDriverMetadata] instance that may have null members
+ */
+ fun getMetadataFromZip(driver: File): GpuDriverMetadata {
+ try {
+ ZipFile(driver).use { zf ->
+ val entries = zf.entries()
+ while (entries.hasMoreElements()) {
+ val entry = entries.nextElement()
+ if (!entry.isDirectory && entry.name.lowercase().contains(".json")) {
+ zf.getInputStream(entry).use {
+ return GpuDriverMetadata(it, entry.size)
+ }
+ }
+ }
+ }
+ } catch (_: ZipException) {
+ }
+ return GpuDriverMetadata()
+ }
+
+ external fun supportsCustomDriverLoading(): Boolean
+
+ // Parse the custom driver metadata to retrieve the name.
+ val customDriverData: GpuDriverMetadata
+ get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
+
+ fun initializeDirectories() {
// Ensure the file redirection directory exists.
val fileRedirectionDir = File(fileRedirectionPath!!)
if (!fileRedirectionDir.exists()) {
@@ -141,14 +211,10 @@ object GpuDriverHelper {
if (!driverInstallationDir.exists()) {
driverInstallationDir.mkdirs()
}
- }
-
- private fun deleteRecursive(fileOrDirectory: File) {
- if (fileOrDirectory.isDirectory) {
- for (child in fileOrDirectory.listFiles()!!) {
- deleteRecursive(child)
- }
+ // Ensure the driver storage directory exists
+ val driverStorageDirectory = File(driverStoragePath)
+ if (!driverStorageDirectory.exists()) {
+ driverStorageDirectory.mkdirs()
}
- fileOrDirectory.delete()
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
index a4e64070a..511a4171a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
@@ -4,29 +4,29 @@
package org.yuzu.yuzu_emu.utils
import java.io.IOException
-import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import java.nio.file.Paths
import org.json.JSONException
import org.json.JSONObject
+import java.io.File
+import java.io.InputStream
-class GpuDriverMetadata(metadataFilePath: String) {
- var name: String? = null
- var description: String? = null
- var author: String? = null
- var vendor: String? = null
- var driverVersion: String? = null
- var minApi = 0
- var libraryName: String? = null
+class GpuDriverMetadata {
+ /**
+ * Tries to get driver metadata information from a meta.json [File]
+ *
+ * @param metadataFile meta.json file provided with a GPU driver
+ */
+ constructor(metadataFile: File) {
+ if (metadataFile.length() > MAX_META_SIZE_BYTES) {
+ return
+ }
- init {
try {
- val json = JSONObject(getStringFromFile(metadataFilePath))
+ val json = JSONObject(FileUtil.getStringFromFile(metadataFile))
name = json.getString("name")
description = json.getString("description")
author = json.getString("author")
vendor = json.getString("vendor")
- driverVersion = json.getString("driverVersion")
+ version = json.getString("driverVersion")
minApi = json.getInt("minApi")
libraryName = json.getString("libraryName")
} catch (e: JSONException) {
@@ -36,12 +36,84 @@ class GpuDriverMetadata(metadataFilePath: String) {
}
}
- companion object {
- @Throws(IOException::class)
- private fun getStringFromFile(filePath: String): String {
- val path = Paths.get(filePath)
- val bytes = Files.readAllBytes(path)
- return String(bytes, StandardCharsets.UTF_8)
+ /**
+ * Tries to get driver metadata information from an input stream that's intended to be
+ * from a zip file
+ *
+ * @param metadataStream ZipEntry input stream
+ * @param size Size of the file in bytes
+ */
+ constructor(metadataStream: InputStream, size: Long) {
+ if (size > MAX_META_SIZE_BYTES) {
+ return
}
+
+ try {
+ val json = JSONObject(FileUtil.getStringFromInputStream(metadataStream))
+ name = json.getString("name")
+ description = json.getString("description")
+ author = json.getString("author")
+ vendor = json.getString("vendor")
+ version = json.getString("driverVersion")
+ minApi = json.getInt("minApi")
+ libraryName = json.getString("libraryName")
+ } catch (e: JSONException) {
+ // JSON is malformed, ignore and treat as unsupported metadata.
+ } catch (e: IOException) {
+ // File is inaccessible, ignore and treat as unsupported metadata.
+ }
+ }
+
+ /**
+ * Creates an empty metadata instance
+ */
+ constructor()
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is GpuDriverMetadata) {
+ return false
+ }
+
+ return other.name == name &&
+ other.description == description &&
+ other.author == author &&
+ other.vendor == vendor &&
+ other.version == version &&
+ other.minApi == minApi &&
+ other.libraryName == libraryName
+ }
+
+ override fun hashCode(): Int {
+ var result = name?.hashCode() ?: 0
+ result = 31 * result + (description?.hashCode() ?: 0)
+ result = 31 * result + (author?.hashCode() ?: 0)
+ result = 31 * result + (vendor?.hashCode() ?: 0)
+ result = 31 * result + (version?.hashCode() ?: 0)
+ result = 31 * result + minApi
+ result = 31 * result + (libraryName?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString(): String =
+ """
+ Name - $name
+ Description - $description
+ Author - $author
+ Vendor - $vendor
+ Version - $version
+ Min API - $minApi
+ Library Name - $libraryName
+ """.trimMargin().trimIndent()
+
+ var name: String? = null
+ var description: String? = null
+ var author: String? = null
+ var vendor: String? = null
+ var version: String? = null
+ var minApi = 0
+ var libraryName: String? = null
+
+ companion object {
+ private const val MAX_META_SIZE_BYTES = 500000
}
}
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp
index a890c6604..a7e414b81 100644
--- a/src/android/app/src/main/jni/emu_window/emu_window.cpp
+++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp
@@ -11,6 +11,12 @@
#include "jni/emu_window/emu_window.h"
void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
+ m_window_width = ANativeWindow_getWidth(surface);
+ m_window_height = ANativeWindow_getHeight(surface);
+
+ // Ensures that we emulate with the correct aspect ratio.
+ UpdateCurrentFramebufferLayout(m_window_width, m_window_height);
+
window_info.render_surface = reinterpret_cast<void*>(surface);
}
@@ -62,14 +68,8 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste
return;
}
- m_window_width = ANativeWindow_getWidth(surface);
- m_window_height = ANativeWindow_getHeight(surface);
-
- // Ensures that we emulate with the correct aspect ratio.
- UpdateCurrentFramebufferLayout(m_window_width, m_window_height);
-
+ OnSurfaceChanged(surface);
window_info.type = Core::Frontend::WindowSystemType::Android;
- window_info.render_surface = reinterpret_cast<void*>(surface);
m_input_subsystem->Initialize();
}
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index f31fe054b..598f4e8bf 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -13,6 +13,8 @@
#include <android/api-level.h>
#include <android/native_window_jni.h>
+#include <common/fs/fs.h>
+#include <core/file_sys/savedata_factory.h>
#include <core/loader/nro.h>
#include <jni.h>
@@ -102,7 +104,7 @@ public:
m_native_window = native_window;
}
- int InstallFileToNand(std::string filename) {
+ int InstallFileToNand(std::string filename, std::string file_extension) {
jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
std::size_t block_size) {
if (src == nullptr || dest == nullptr) {
@@ -134,15 +136,11 @@ public:
m_system.GetFileSystemController().CreateFactories(*m_vfs);
[[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
- if (filename.ends_with("nsp")) {
+ if (file_extension == "nsp") {
nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
if (nsp->IsExtractedType()) {
return InstallError;
}
- } else if (filename.ends_with("xci")) {
- jconst xci =
- std::make_shared<FileSys::XCI>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
- nsp = xci->GetSecurePartitionNSP();
} else {
return ErrorFilenameExtension;
}
@@ -220,7 +218,6 @@ public:
return;
}
m_window->OnSurfaceChanged(m_native_window);
- m_system.Renderer().NotifySurfaceChanged();
}
void ConfigureFilesystemProvider(const std::string& filepath) {
@@ -607,8 +604,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject
}
int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
- [[maybe_unused]] jstring j_file) {
- return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file));
+ jstring j_file,
+ jstring j_file_extension) {
+ return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file),
+ GetJString(env, j_file_extension));
}
void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
@@ -879,4 +878,24 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env
EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
}
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* env,
+ jobject instance) {
+ const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
+ auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory(
+ Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read);
+
+ Service::Account::ProfileManager manager;
+ const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
+ ASSERT(user_id);
+
+ const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
+ EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser,
+ FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0);
+
+ const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path);
+ if (!Common::FS::CreateParentDirs(full_path)) {
+ LOG_WARNING(Frontend, "Failed to create full path of the default user's save directory");
+ }
+}
+
} // extern "C"
diff --git a/src/android/app/src/main/res/drawable/ic_build.xml b/src/android/app/src/main/res/drawable/ic_build.xml
new file mode 100644
index 000000000..91d52f1b8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_build.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_delete.xml b/src/android/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 000000000..d26a79711
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_export.xml b/src/android/app/src/main/res/drawable/ic_export.xml
new file mode 100644
index 000000000..463d2f41c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_export.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_import.xml b/src/android/app/src/main/res/drawable/ic_import.xml
new file mode 100644
index 000000000..3a99dd5e6
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_import.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
+</vector>
diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml
index 8a026a30a..a187665f2 100644
--- a/src/android/app/src/main/res/layout/activity_settings.xml
+++ b/src/android/app/src/main/res/layout/activity_settings.xml
@@ -22,7 +22,7 @@
<View
android:id="@+id/navigation_bar_shade"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="1px"
android:background="@android:color/transparent"
android:clickable="false"
diff --git a/src/android/app/src/main/res/layout/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml
new file mode 100644
index 000000000..1dd9a6d7d
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_driver_option.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/materialCardViewOutlinedStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginVertical="12dp"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="center"
+ android:padding="16dp">
+
+ <RadioButton
+ android:id="@+id/radio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:clickable="false"
+ android:checked="false" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/title"
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ tools:text="@string/select_gpu_driver_default" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/version"
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:ellipsize="none"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ tools:text="@string/install_gpu_driver_description" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/description"
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:ellipsize="none"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ tools:text="@string/install_gpu_driver_description" />
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/button_delete"
+ style="@style/Widget.Material3.Button.IconButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@string/delete"
+ android:tooltipText="@string/delete"
+ app:icon="@drawable/ic_delete"
+ app:iconTint="?attr/colorControlNormal" />
+
+ </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml
new file mode 100644
index 000000000..f5b0e3741
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_installable.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/materialCardViewOutlinedStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginVertical="12dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:orientation="horizontal"
+ android:layout_gravity="center">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/title"
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/user_data"
+ android:textAlignment="viewStart" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/description"
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:text="@string/user_data_description"
+ android:textAlignment="viewStart" />
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/button_export"
+ style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@string/export"
+ android:tooltipText="@string/export"
+ android:visibility="gone"
+ app:icon="@drawable/ic_export"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/button_install"
+ style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginStart="12dp"
+ android:contentDescription="@string/string_import"
+ android:tooltipText="@string/string_import"
+ android:visibility="gone"
+ app:icon="@drawable/ic_import"
+ tools:visibility="visible" />
+
+ </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
index d17711a65..0209ea082 100644
--- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml
+++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
@@ -1,24 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
+<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:orientation="vertical">
-
- <com.google.android.material.progressindicator.LinearProgressIndicator
- android:id="@+id/progress_bar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="24dp"
- app:trackCornerRadius="4dp" />
-
- <TextView
- android:id="@+id/progress_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginLeft="24dp"
- android:layout_marginRight="24dp"
- android:layout_marginBottom="24dp"
- android:gravity="end" />
-
-</LinearLayout>
+ android:id="@+id/progress_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="24dp"
+ app:trackCornerRadius="4dp" />
diff --git a/src/android/app/src/main/res/layout/fragment_driver_manager.xml b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
new file mode 100644
index 000000000..6cea2d164
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_drivers"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ app:liftOnScrollTargetViewId="@id/list_drivers">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_drivers"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:navigationIcon="@drawable/ic_back"
+ app:title="@string/gpu_driver_manager" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_drivers"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+ <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+ android:id="@+id/button_install"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:text="@string/install"
+ app:icon="@drawable/ic_add"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
index da97d85c1..750ce094a 100644
--- a/src/android/app/src/main/res/layout/fragment_emulation.xml
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -32,7 +32,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:focusable="false">
+ android:focusable="false"
+ android:clickable="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loading_layout"
@@ -155,7 +156,7 @@
android:id="@+id/in_game_menu"
android:layout_width="wrap_content"
android:layout_height="match_parent"
- android:layout_gravity="start|bottom"
+ android:layout_gravity="start"
app:headerLayout="@layout/header_in_game"
app:menu="@menu/menu_in_game"
tools:visibility="gone" />
diff --git a/src/android/app/src/main/res/layout/fragment_installables.xml b/src/android/app/src/main/res/layout/fragment_installables.xml
new file mode 100644
index 000000000..3a4df81a6
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_installables.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_installables"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_installables"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:title="@string/manage_yuzu_data"
+ app:navigationIcon="@drawable/ic_back" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_installables"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index 2e0ce7a3d..82749359d 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -19,6 +19,12 @@
<action
android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment"
app:destination="@id/earlyAccessFragment" />
+ <action
+ android:id="@+id/action_homeSettingsFragment_to_installableFragment"
+ app:destination="@id/installableFragment" />
+ <action
+ android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment"
+ app:destination="@id/driverManagerFragment" />
</fragment>
<fragment
@@ -88,5 +94,13 @@
<action
android:id="@+id/action_global_settingsActivity"
app:destination="@id/settingsActivity" />
+ <fragment
+ android:id="@+id/installableFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"
+ android:label="InstallableFragment" />
+ <fragment
+ android:id="@+id/driverManagerFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
+ android:label="DriverManagerFragment" />
</navigation>
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml
index daaa7ffde..72a47fbdb 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -79,7 +79,6 @@
<string name="manage_save_data">Speicherdaten verwalten</string>
<string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string>
<string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string>
- <string name="import_export_saves_no_profile">Keine Speicherdaten gefunden. Bitte starte ein Spiel und versuche es erneut.</string>
<string name="save_file_imported_success">Erfolgreich importiert</string>
<string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string>
<string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</string>
@@ -169,9 +168,7 @@
<string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string>
<string name="select_gpu_driver_install">Installieren</string>
<string name="select_gpu_driver_default">Standard</string>
- <string name="select_gpu_driver_install_success">%s wurde installiert</string>
<string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string>
- <string name="select_gpu_driver_error">Ungültiger Treiber ausgewählt, Standard-Treiber wird verwendet!</string>
<string name="system_gpu_driver">System GPU-Treiber</string>
<string name="installing_driver">Treiber wird installiert...</string>
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml
index e9129cb00..e5bdd5889 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Administrar datos de guardado</string>
<string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string>
<string name="import_export_saves_description">Importar o exportar archivos de guardado</string>
- <string name="import_export_saves_no_profile">No se han encontrado datos de guardado. Por favor, ejecute un juego y vuelva a intentarlo.</string>
<string name="save_file_imported_success">Importado correctamente</string>
<string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string>
<string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string>
<string name="select_gpu_driver_install">Instalar</string>
<string name="select_gpu_driver_default">Predeterminado</string>
- <string name="select_gpu_driver_install_success">Instalado %s</string>
<string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string>
- <string name="select_gpu_driver_error">¡Driver no válido, utilizando el predeterminado del sistema!</string>
<string name="system_gpu_driver">Driver GPU del sistema</string>
<string name="installing_driver">Instalando driver...</string>
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml
index 2d99d618e..1e02828aa 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Gérer les données de sauvegarde</string>
<string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string>
<string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string>
- <string name="import_export_saves_no_profile">Aucune données de sauvegarde trouvées. Veuillez lancer un jeu et réessayer.</string>
<string name="save_file_imported_success">Importé avec succès</string>
<string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string>
<string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string>
<string name="select_gpu_driver_install">Installer</string>
<string name="select_gpu_driver_default">Défaut</string>
- <string name="select_gpu_driver_install_success">%s Installé</string>
<string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string>
- <string name="select_gpu_driver_error">Pilote non valide sélectionné, utilisation du paramètre par défaut du système !</string>
<string name="system_gpu_driver">Pilote du GPU du système</string>
<string name="installing_driver">Installation du pilote...</string>
diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml
index d9c3de385..09c9345b0 100644
--- a/src/android/app/src/main/res/values-it/strings.xml
+++ b/src/android/app/src/main/res/values-it/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Gestisci i salvataggi</string>
<string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string>
<string name="import_export_saves_description">Importa o esporta i salvataggi</string>
- <string name="import_export_saves_no_profile">Nessun salvataggio trovato. Avvia un gioco e riprova.</string>
<string name="save_file_imported_success">Importato con successo</string>
<string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string>
<string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string>
<string name="select_gpu_driver_install">Installa</string>
<string name="select_gpu_driver_default">Predefinito</string>
- <string name="select_gpu_driver_install_success">Installato%s</string>
<string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string>
- <string name="select_gpu_driver_error">Il driver selezionato è invalido, è in utilizzo quello predefinito di sistema!</string>
<string name="system_gpu_driver">Driver GPU del sistema</string>
<string name="installing_driver">Installando i driver...</string>
diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml
index 7a226cd5c..a0ea78bef 100644
--- a/src/android/app/src/main/res/values-ja/strings.xml
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -80,7 +80,6 @@
<string name="manage_save_data">セーブデータを管理</string>
<string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string>
<string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string>
- <string name="import_export_saves_no_profile">セーブデータがありません。ゲームを起動してから再度お試しください。</string>
<string name="save_file_imported_success">インポートが完了しました</string>
<string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string>
<string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</string>
@@ -171,9 +170,7 @@
<string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string>
<string name="select_gpu_driver_install">インストール</string>
<string name="select_gpu_driver_default">デフォルト</string>
- <string name="select_gpu_driver_install_success">%s をインストールしました</string>
<string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string>
- <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string>
<string name="system_gpu_driver">システムのGPUドライバ</string>
<string name="installing_driver">インストール中…</string>
diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml
index 427b6e5a0..214f95706 100644
--- a/src/android/app/src/main/res/values-ko/strings.xml
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">저장 데이터 관리</string>
<string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string>
<string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string>
- <string name="import_export_saves_no_profile">저장 데이터를 찾을 수 없습니다. 게임을 실행한 후 다시 시도하세요.</string>
<string name="save_file_imported_success">가져오기 성공</string>
<string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string>
<string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string>
<string name="select_gpu_driver_install">설치</string>
<string name="select_gpu_driver_default">기본값</string>
- <string name="select_gpu_driver_install_success">설치된 %s</string>
<string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string>
- <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string>
<string name="system_gpu_driver">시스템 GPU 드라이버</string>
<string name="installing_driver">드라이버 설치 중...</string>
diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml
index ce8d7a9e4..5443cef42 100644
--- a/src/android/app/src/main/res/values-nb/strings.xml
+++ b/src/android/app/src/main/res/values-nb/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Administrere lagringsdata</string>
<string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string>
<string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string>
- <string name="import_export_saves_no_profile">Ingen lagringsdata funnet. Start et nytt spill og prøv på nytt.</string>
<string name="save_file_imported_success">Vellykket import</string>
<string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string>
<string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string>
<string name="select_gpu_driver_install">Installer</string>
<string name="select_gpu_driver_default">Standard</string>
- <string name="select_gpu_driver_install_success">Installert %s</string>
<string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string>
- <string name="select_gpu_driver_error">Ugyldig driver valgt, bruker systemstandard!</string>
<string name="system_gpu_driver">Systemets GPU-driver</string>
<string name="installing_driver">Installerer driver...</string>
diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml
index c2c24b48f..899e233d0 100644
--- a/src/android/app/src/main/res/values-pl/strings.xml
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Zarządzaj plikami zapisów gier</string>
<string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string>
<string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string>
- <string name="import_export_saves_no_profile">Nie znaleziono plików zapisów. Uruchom grę i spróbuj ponownie.</string>
<string name="save_file_imported_success">Zaimportowano pomyślnie</string>
<string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string>
<string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string>
<string name="select_gpu_driver_install">Zainstaluj</string>
<string name="select_gpu_driver_default">Domyślne</string>
- <string name="select_gpu_driver_install_success">Zainstalowano %s</string>
<string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string>
- <string name="select_gpu_driver_error">Wybrano błędny sterownik, powrót do domyślnego. </string>
<string name="system_gpu_driver">Systemowy sterownik GPU</string>
<string name="installing_driver">Instalowanie sterownika...</string>
diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml
index 04f276108..caa095364 100644
--- a/src/android/app/src/main/res/values-pt-rBR/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Gerir dados guardados</string>
<string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>
<string name="import_export_saves_description">Importa ou exporta dados guardados</string>
- <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>
<string name="save_file_imported_success">Importado com sucesso</string>
<string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>
<string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
<string name="select_gpu_driver_install">Instalar</string>
<string name="select_gpu_driver_default">Padrão</string>
- <string name="select_gpu_driver_install_success">Instalado%s</string>
<string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
- <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
<string name="system_gpu_driver">Driver do GPU padrão</string>
<string name="installing_driver">A instalar o Driver...</string>
diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml
index 66a3a1a2e..0a1a47fbb 100644
--- a/src/android/app/src/main/res/values-pt-rPT/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Gerir dados guardados</string>
<string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>
<string name="import_export_saves_description">Importa ou exporta dados guardados</string>
- <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>
<string name="save_file_imported_success">Importado com sucesso</string>
<string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>
<string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
<string name="select_gpu_driver_install">Instalar</string>
<string name="select_gpu_driver_default">Padrão</string>
- <string name="select_gpu_driver_install_success">Instalado%s</string>
<string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
- <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
<string name="system_gpu_driver">Driver do GPU padrão</string>
<string name="installing_driver">A instalar o Driver...</string>
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml
index f770e954f..0bef035d6 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Управление данными сохранений</string>
<string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string>
<string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string>
- <string name="import_export_saves_no_profile">Данные сохранений не найдены. Пожалуйста, запустите игру и повторите попытку.</string>
<string name="save_file_imported_success">Успешно импортировано</string>
<string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string>
<string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string>
<string name="select_gpu_driver_install">Установить</string>
<string name="select_gpu_driver_default">По умолчанию</string>
- <string name="select_gpu_driver_install_success">Установлено %s</string>
<string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string>
- <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string>
<string name="system_gpu_driver">Системный драйвер ГП</string>
<string name="installing_driver">Установка драйвера...</string>
diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml
index ea3ab1b15..5b789ee98 100644
--- a/src/android/app/src/main/res/values-uk/strings.xml
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">Керування даними збережень</string>
<string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string>
<string name="import_export_saves_description">Імпорт або експорт файлів збереження</string>
- <string name="import_export_saves_no_profile">Дані збережень не знайдено. Будь ласка, запустіть гру та повторіть спробу.</string>
<string name="save_file_imported_success">Успішно імпортовано</string>
<string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string>
<string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string>
<string name="select_gpu_driver_install">Встановити</string>
<string name="select_gpu_driver_default">За замовчуванням</string>
- <string name="select_gpu_driver_install_success">Встановлено %s</string>
<string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string>
- <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string>
<string name="system_gpu_driver">Системний драйвер ГП</string>
<string name="installing_driver">Встановлення драйвера...</string>
diff --git a/src/android/app/src/main/res/values-w600dp/integers.xml b/src/android/app/src/main/res/values-w600dp/integers.xml
new file mode 100644
index 000000000..9975db801
--- /dev/null
+++ b/src/android/app/src/main/res/values-w600dp/integers.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <integer name="grid_columns">2</integer>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml
index b45a5a528..c0e885751 100644
--- a/src/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">管理存档数据</string>
<string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string>
<string name="import_export_saves_description">导入或导出存档</string>
- <string name="import_export_saves_no_profile">找不到存档数据,请启动游戏并重试。</string>
<string name="save_file_imported_success">已成功导入存档</string>
<string name="save_file_invalid_zip_structure">无效的存档目录</string>
<string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string>
<string name="select_gpu_driver_install">安装</string>
<string name="select_gpu_driver_default">系统默认</string>
- <string name="select_gpu_driver_install_success">已安装 %s</string>
<string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string>
- <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string>
<string name="system_gpu_driver">系统 GPU 驱动程序</string>
<string name="installing_driver">正在安装驱动程序…</string>
diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml
index 3aab889e4..4a21bf893 100644
--- a/src/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -81,7 +81,6 @@
<string name="manage_save_data">管理儲存資料</string>
<string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string>
<string name="import_export_saves_description">匯入或匯出儲存檔案</string>
- <string name="import_export_saves_no_profile">找不到儲存資料,請啟動遊戲並重試。</string>
<string name="save_file_imported_success">已成功匯入</string>
<string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string>
<string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string>
@@ -172,9 +171,7 @@
<string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string>
<string name="select_gpu_driver_install">安裝</string>
<string name="select_gpu_driver_default">預設</string>
- <string name="select_gpu_driver_install_success">已安裝 %s</string>
<string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string>
- <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string>
<string name="system_gpu_driver">系統 GPU 驅動程式</string>
<string name="installing_driver">正在安裝驅動程式…</string>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index 7b2296d95..ef855ea6f 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -13,6 +13,8 @@
<dimen name="menu_width">256dp</dimen>
<dimen name="card_width">165dp</dimen>
<dimen name="icon_inset">24dp</dimen>
+ <dimen name="spacing_bottom_list_fab">72dp</dimen>
+ <dimen name="spacing_fab">24dp</dimen>
<dimen name="dialog_margin">20dp</dimen>
<dimen name="elevated_app_bar">3dp</dimen>
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
index 5e39bc7d9..dc527965c 100644
--- a/src/android/app/src/main/res/values/integers.xml
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <integer name="game_title_lines">2</integer>
+ <integer name="grid_columns">1</integer>
<!-- Default SWITCH landscape layout -->
<integer name="SWITCH_BUTTON_A_X">760</integer>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index b163e6fc1..9e4854221 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -72,6 +72,7 @@
<string name="invalid_keys_error">Invalid encryption keys</string>
<string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
<string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
+ <string name="gpu_driver_manager">GPU Driver Manager</string>
<string name="install_gpu_driver">Install GPU driver</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
<string name="advanced_settings">Advanced settings</string>
@@ -90,7 +91,6 @@
<string name="manage_save_data">Manage save data</string>
<string name="manage_save_data_description">Save data found. Please select an option below.</string>
<string name="import_export_saves_description">Import or export save files</string>
- <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string>
<string name="save_file_imported_success">Imported successfully</string>
<string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
<string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string>
@@ -101,12 +101,13 @@
<string name="firmware_installing">Installing firmware</string>
<string name="firmware_installed_success">Firmware installed successfully</string>
<string name="firmware_installed_failure">Firmware installation failed</string>
- <string name="firmware_installed_failure_description">Verify that the ZIP contains valid firmware and try again.</string>
+ <string name="firmware_installed_failure_description">Make sure the firmware nca files are at the root of the zip and try again.</string>
<string name="share_log">Share debug logs</string>
<string name="share_log_description">Share yuzu\'s log file to debug issues</string>
<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="installing_game_content">Installing content…</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>
@@ -118,6 +119,10 @@
<string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string>
<string name="custom_driver_not_supported">Custom drivers not supported</string>
<string name="custom_driver_not_supported_description">Custom driver loading isn\'t currently supported for this device.\nCheck this option again in the future to see if support was added!</string>
+ <string name="manage_yuzu_data">Manage yuzu data</string>
+ <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
+ <string name="share_save_file">Share save file</string>
+ <string name="export_save_failed">Failed to export save</string>
<!-- About screen strings -->
<string name="gaia_is_not_real">Gaia isn\'t real</string>
@@ -128,6 +133,16 @@
<string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
<string name="licenses_description">Projects that make yuzu for Android possible</string>
<string name="build">Build</string>
+ <string name="user_data">User data</string>
+ <string name="user_data_description">Import/export all app data.\n\nWhen importing user data, all existing user data will be deleted!</string>
+ <string name="exporting_user_data">Exporting user data…</string>
+ <string name="importing_user_data">Importing user data…</string>
+ <string name="import_user_data">Import user data</string>
+ <string name="invalid_yuzu_backup">Invalid yuzu backup</string>
+ <string name="user_data_export_success">User data exported successfully</string>
+ <string name="user_data_import_success">User data imported successfully</string>
+ <string name="user_data_export_cancelled">Export cancelled</string>
+ <string name="user_data_import_failed_description">Make sure the user data folders are at the root of the zip folder and contain a config file at config/config.ini and try again.</string>
<string name="support_link">https://discord.gg/u77vRWY</string>
<string name="website_link">https://yuzu-emu.org/</string>
<string name="github_link">https://github.com/yuzu-emu</string>
@@ -215,15 +230,22 @@
<string name="auto">Auto</string>
<string name="submit">Submit</string>
<string name="string_null">Null</string>
+ <string name="string_import">Import</string>
+ <string name="export">Export</string>
+ <string name="export_failed">Export failed</string>
+ <string name="import_failed">Import failed</string>
+ <string name="cancelling">Cancelling</string>
+ <string name="install">Install</string>
+ <string name="delete">Delete</string>
<!-- GPU driver installation -->
<string name="select_gpu_driver">Select GPU driver</string>
<string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>
<string name="select_gpu_driver_install">Install</string>
<string name="select_gpu_driver_default">Default</string>
- <string name="select_gpu_driver_install_success">Installed %s</string>
<string name="select_gpu_driver_use_default">Using default GPU driver</string>
- <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string>
+ <string name="select_gpu_driver_error">Invalid driver selected</string>
+ <string name="driver_already_installed">Driver already installed</string>
<string name="system_gpu_driver">System GPU driver</string>
<string name="installing_driver">Installing driver…</string>
@@ -281,6 +303,7 @@
<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>
<string name="memory_formatted">%1$s %2$s</string>
+ <string name="no_game_present">No bootable game present!</string>
<!-- Region Names -->
<string name="region_japan">Japan</string>
diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts
index 80f370c16..51e559321 100644
--- a/src/android/build.gradle.kts
+++ b/src/android/build.gradle.kts
@@ -3,8 +3,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id("com.android.application") version "8.0.2" apply false
- id("com.android.library") version "8.0.2" apply false
+ id("com.android.application") version "8.1.2" apply false
+ id("com.android.library") version "8.1.2" apply false
id("org.jetbrains.kotlin.android") version "1.8.21" apply false
}