summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-04-05 20:26:53 -0400
committerGravatar bunnei2023-06-03 00:05:51 -0700
commit233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38 (patch)
treea9e82ac5af26b935ca0d5aeb9e7be9f780667369 /src/android
parentandroid: Enforce Vulkan 1.1 support as minimum (diff)
downloadyuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.gz
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.xz
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.zip
android: MainActivity overhaul
This moves several parts of the main activity into fragments that manage themselves to react to changes. UI changes like the appearance of a new search view or when the games list changes now gets updated via multiple view models. This also starts a conversion to the androidx navigation component which furthers the goals mentioned previously with more fragment responsibility. This will eventually allow us to use one activity with interchanging fragments and multiple view models that are stored within that central activity. fdas
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/build.gradle.kts3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt42
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt55
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt281
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt50
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt220
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt307
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt52
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt23
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt109
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt48
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt10
-rw-r--r--src/android/app/src/main/res/drawable/ic_add.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_input.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_nfc.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_options.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_unlock.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_yuzu_themed.xml18
-rw-r--r--src/android/app/src/main/res/layout/activity_main.xml42
-rw-r--r--src/android/app/src/main/res/layout/card_home_option.xml53
-rw-r--r--src/android/app/src/main/res/layout/fragment_games.xml80
-rw-r--r--src/android/app/src/main/res/layout/fragment_grid.xml37
-rw-r--r--src/android/app/src/main/res/layout/fragment_options.xml30
-rw-r--r--src/android/app/src/main/res/menu/menu_game_grid.xml47
-rw-r--r--src/android/app/src/main/res/menu/menu_navigation.xml14
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml17
-rw-r--r--src/android/app/src/main/res/values/dimens.xml7
-rw-r--r--src/android/app/src/main/res/values/strings.xml29
32 files changed, 1031 insertions, 626 deletions
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index 552d4a721..d8ef02ac1 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -155,6 +155,9 @@ dependencies {
155 implementation("org.ini4j:ini4j:0.5.4") 155 implementation("org.ini4j:ini4j:0.5.4")
156 implementation("androidx.constraintlayout:constraintlayout:2.1.4") 156 implementation("androidx.constraintlayout:constraintlayout:2.1.4")
157 implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") 157 implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
158 implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
159 implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
160 implementation("info.debatty:java-string-similarity:2.0.0")
158} 161}
159 162
160fun getVersion(): String { 163fun getVersion(): String {
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 f1f92841c..fd174fd2d 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
@@ -13,7 +13,6 @@ import android.view.View
13import android.view.WindowManager 13import android.view.WindowManager
14import android.view.inputmethod.InputMethodManager 14import android.view.inputmethod.InputMethodManager
15import androidx.appcompat.app.AppCompatActivity 15import androidx.appcompat.app.AppCompatActivity
16import androidx.fragment.app.FragmentActivity
17import androidx.preference.PreferenceManager 16import androidx.preference.PreferenceManager
18import com.google.android.material.dialog.MaterialAlertDialogBuilder 17import com.google.android.material.dialog.MaterialAlertDialogBuilder
19import com.google.android.material.slider.Slider.OnChangeListener 18import com.google.android.material.slider.Slider.OnChangeListener
@@ -202,7 +201,7 @@ open class EmulationActivity : AppCompatActivity() {
202 private const val EMULATION_RUNNING_NOTIFICATION = 0x1000 201 private const val EMULATION_RUNNING_NOTIFICATION = 0x1000
203 202
204 @JvmStatic 203 @JvmStatic
205 fun launch(activity: FragmentActivity, game: Game) { 204 fun launch(activity: AppCompatActivity, game: Game) {
206 val launcher = Intent(activity, EmulationActivity::class.java) 205 val launcher = Intent(activity, EmulationActivity::class.java)
207 launcher.putExtra(EXTRA_SELECTED_GAME, game) 206 launcher.putExtra(EXTRA_SELECTED_GAME, game)
208 activity.startActivity(launcher) 207 activity.startActivity(launcher)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index af83f05c1..1102b60b1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -3,6 +3,7 @@
3 3
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.annotation.SuppressLint
6import android.graphics.Bitmap 7import android.graphics.Bitmap
7import android.graphics.BitmapFactory 8import android.graphics.BitmapFactory
8import android.view.LayoutInflater 9import android.view.LayoutInflater
@@ -11,29 +12,25 @@ import android.view.ViewGroup
11import android.widget.ImageView 12import android.widget.ImageView
12import androidx.appcompat.app.AppCompatActivity 13import androidx.appcompat.app.AppCompatActivity
13import androidx.lifecycle.lifecycleScope 14import androidx.lifecycle.lifecycleScope
15import androidx.recyclerview.widget.AsyncDifferConfig
16import androidx.recyclerview.widget.DiffUtil
17import androidx.recyclerview.widget.ListAdapter
14import androidx.recyclerview.widget.RecyclerView 18import androidx.recyclerview.widget.RecyclerView
15import coil.load 19import coil.load
16import kotlinx.coroutines.Dispatchers
17import kotlinx.coroutines.launch 20import kotlinx.coroutines.launch
18import kotlinx.coroutines.withContext
19import org.yuzu.yuzu_emu.NativeLibrary 21import org.yuzu.yuzu_emu.NativeLibrary
20import org.yuzu.yuzu_emu.R 22import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.databinding.CardGameBinding 23import org.yuzu.yuzu_emu.databinding.CardGameBinding
22import org.yuzu.yuzu_emu.activities.EmulationActivity 24import org.yuzu.yuzu_emu.activities.EmulationActivity
23import org.yuzu.yuzu_emu.model.Game 25import org.yuzu.yuzu_emu.model.Game
24import kotlin.collections.ArrayList 26import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
25 27
26/** 28class GameAdapter(private val activity: AppCompatActivity) :
27 * This adapter gets its information from a database Cursor. This fact, paired with the usage of 29 ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
28 * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
29 * large dataset.
30 */
31class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<Game>) :
32 RecyclerView.Adapter<GameAdapter.GameViewHolder>(),
33 View.OnClickListener { 30 View.OnClickListener {
34 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { 31 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
35 // Create a new view. 32 // Create a new view.
36 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) 33 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
37 binding.root.setOnClickListener(this) 34 binding.root.setOnClickListener(this)
38 35
39 // Use that view to create a ViewHolder. 36 // Use that view to create a ViewHolder.
@@ -41,12 +38,10 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
41 } 38 }
42 39
43 override fun onBindViewHolder(holder: GameViewHolder, position: Int) { 40 override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
44 holder.bind(games[position]) 41 holder.bind(currentList[position])
45 } 42 }
46 43
47 override fun getItemCount(): Int { 44 override fun getItemCount(): Int = currentList.size
48 return games.size
49 }
50 45
51 /** 46 /**
52 * Launches the game that was clicked on. 47 * Launches the game that was clicked on.
@@ -55,7 +50,7 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
55 */ 50 */
56 override fun onClick(view: View) { 51 override fun onClick(view: View) {
57 val holder = view.tag as GameViewHolder 52 val holder = view.tag as GameViewHolder
58 EmulationActivity.launch((view.context as AppCompatActivity), holder.game) 53 EmulationActivity.launch(activity, holder.game)
59 } 54 }
60 55
61 inner class GameViewHolder(val binding: CardGameBinding) : 56 inner class GameViewHolder(val binding: CardGameBinding) :
@@ -74,7 +69,6 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
74 val bitmap = decodeGameIcon(game.path) 69 val bitmap = decodeGameIcon(game.path)
75 binding.imageGameScreen.load(bitmap) { 70 binding.imageGameScreen.load(bitmap) {
76 error(R.drawable.no_icon) 71 error(R.drawable.no_icon)
77 crossfade(true)
78 } 72 }
79 } 73 }
80 74
@@ -87,9 +81,15 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
87 } 81 }
88 } 82 }
89 83
90 fun swapData(games: ArrayList<Game>) { 84 private class DiffCallback : DiffUtil.ItemCallback<Game>() {
91 this.games = games 85 override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
92 notifyDataSetChanged() 86 return oldItem.gameId == newItem.gameId
87 }
88
89 @SuppressLint("DiffUtilEquals")
90 override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
91 return oldItem == newItem
92 }
93 } 93 }
94 94
95 private fun decodeGameIcon(uri: String): Bitmap? { 95 private fun decodeGameIcon(uri: String): Bitmap? {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt
new file mode 100644
index 000000000..2bec2de87
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt
@@ -0,0 +1,55 @@
1package org.yuzu.yuzu_emu.adapters
2
3import android.view.LayoutInflater
4import android.view.View
5import android.view.ViewGroup
6import androidx.appcompat.app.AppCompatActivity
7import androidx.core.content.res.ResourcesCompat
8import androidx.recyclerview.widget.RecyclerView
9import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
10import org.yuzu.yuzu_emu.model.HomeOption
11
12class HomeOptionAdapter(private val activity: AppCompatActivity, var options: List<HomeOption>) :
13 RecyclerView.Adapter<HomeOptionAdapter.HomeOptionViewHolder>(),
14 View.OnClickListener {
15 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
16 val binding = CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
17 binding.root.setOnClickListener(this)
18 return HomeOptionViewHolder(binding)
19 }
20
21 override fun getItemCount(): Int {
22 return options.size
23 }
24
25 override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
26 holder.bind(options[position])
27 }
28
29 override fun onClick(view: View) {
30 val holder = view.tag as HomeOptionViewHolder
31 holder.option.onClick.invoke()
32 }
33
34 inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
35 RecyclerView.ViewHolder(binding.root) {
36 lateinit var option: HomeOption
37
38 init {
39 itemView.tag = this
40 }
41
42 fun bind(option: HomeOption) {
43 this.option = option
44 binding.optionTitle.text = activity.resources.getString(option.titleId)
45 binding.optionDescription.text = activity.resources.getString(option.descriptionId)
46 binding.optionIcon.setImageDrawable(
47 ResourcesCompat.getDrawable(
48 activity.resources,
49 option.iconId,
50 activity.theme
51 )
52 )
53 }
54 }
55}
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 0f2c23827..e4bdcc991 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
@@ -15,6 +15,7 @@ import androidx.core.view.ViewCompat
15import androidx.core.view.WindowCompat 15import androidx.core.view.WindowCompat
16import androidx.core.view.WindowInsetsCompat 16import androidx.core.view.WindowInsetsCompat
17import androidx.core.view.updatePadding 17import androidx.core.view.updatePadding
18import com.google.android.material.color.MaterialColors
18import org.yuzu.yuzu_emu.NativeLibrary 19import org.yuzu.yuzu_emu.NativeLibrary
19import org.yuzu.yuzu_emu.R 20import org.yuzu.yuzu_emu.R
20import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding 21import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@@ -50,6 +51,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
50 setSupportActionBar(binding.toolbarSettings) 51 setSupportActionBar(binding.toolbarSettings)
51 supportActionBar!!.setDisplayHomeAsUpEnabled(true) 52 supportActionBar!!.setDisplayHomeAsUpEnabled(true)
52 53
54 ThemeHelper.setNavigationBarColor(
55 this,
56 MaterialColors.getColor(window.decorView, R.attr.colorSurface)
57 )
58
53 setInsets() 59 setInsets()
54 } 60 }
55 61
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt
new file mode 100644
index 000000000..dac9e67d5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt
@@ -0,0 +1,281 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.content.DialogInterface
7import android.content.Intent
8import android.os.Bundle
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import android.widget.Toast
13import androidx.activity.result.contract.ActivityResultContracts
14import androidx.appcompat.app.AppCompatActivity
15import androidx.core.view.ViewCompat
16import androidx.core.view.WindowInsetsCompat
17import androidx.fragment.app.Fragment
18import androidx.fragment.app.activityViewModels
19import androidx.lifecycle.lifecycleScope
20import androidx.preference.PreferenceManager
21import androidx.recyclerview.widget.LinearLayoutManager
22import com.google.android.material.dialog.MaterialAlertDialogBuilder
23import kotlinx.coroutines.Dispatchers
24import kotlinx.coroutines.launch
25import kotlinx.coroutines.withContext
26import org.yuzu.yuzu_emu.NativeLibrary
27import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter
29import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
30import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding
31import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
32import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
33import org.yuzu.yuzu_emu.model.GamesViewModel
34import org.yuzu.yuzu_emu.model.HomeOption
35import org.yuzu.yuzu_emu.utils.DirectoryInitialization
36import org.yuzu.yuzu_emu.utils.FileUtil
37import org.yuzu.yuzu_emu.utils.GameHelper
38import org.yuzu.yuzu_emu.utils.GpuDriverHelper
39import java.io.IOException
40
41class OptionsFragment : Fragment() {
42 private var _binding: FragmentOptionsBinding? = null
43 private val binding get() = _binding!!
44
45 private val gamesViewModel: GamesViewModel by activityViewModels()
46
47 override fun onCreateView(
48 inflater: LayoutInflater,
49 container: ViewGroup?,
50 savedInstanceState: Bundle?
51 ): View {
52 _binding = FragmentOptionsBinding.inflate(layoutInflater)
53 return binding.root
54 }
55
56 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57 val optionsList: List<HomeOption> = listOf(
58 HomeOption(
59 R.string.add_games,
60 R.string.add_games_description,
61 R.drawable.ic_add
62 ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
63 HomeOption(
64 R.string.install_prod_keys,
65 R.string.install_prod_keys_description,
66 R.drawable.ic_unlock
67 ) { getProdKey.launch(arrayOf("*/*")) },
68 HomeOption(
69 R.string.install_amiibo_keys,
70 R.string.install_amiibo_keys_description,
71 R.drawable.ic_nfc
72 ) { getAmiiboKey.launch(arrayOf("*/*")) },
73 HomeOption(
74 R.string.install_gpu_driver,
75 R.string.install_gpu_driver_description,
76 R.drawable.ic_input
77 ) { driverInstaller() },
78 HomeOption(
79 R.string.settings,
80 R.string.settings_description,
81 R.drawable.ic_settings
82 ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") }
83 )
84
85 binding.optionsList.apply {
86 layoutManager = LinearLayoutManager(requireContext())
87 adapter = HomeOptionAdapter(requireActivity() as AppCompatActivity, optionsList)
88 }
89
90 requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity(
91 MaterialColors.getColor(
92 binding.root,
93 R.attr.colorSurface
94 ), ThemeHelper.SYSTEM_BAR_ALPHA
95 )
96
97 setInsets()
98 }
99
100 override fun onDestroyView() {
101 super.onDestroyView()
102 _binding = null
103 }
104
105 private fun driverInstaller() {
106 // Get the driver name for the dialog message.
107 var driverName = GpuDriverHelper.customDriverName
108 if (driverName == null) {
109 driverName = getString(R.string.system_gpu_driver)
110 }
111
112 MaterialAlertDialogBuilder(requireContext())
113 .setTitle(getString(R.string.select_gpu_driver_title))
114 .setMessage(driverName)
115 .setNegativeButton(android.R.string.cancel, null)
116 .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
117 GpuDriverHelper.installDefaultDriver(requireContext())
118 Toast.makeText(
119 requireContext(),
120 R.string.select_gpu_driver_use_default,
121 Toast.LENGTH_SHORT
122 ).show()
123 }
124 .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
125 getDriver.launch(arrayOf("application/zip"))
126 }
127 .show()
128 }
129
130 private fun setInsets() =
131 ViewCompat.setOnApplyWindowInsetsListener(binding.scrollViewOptions) { view: View, windowInsets: WindowInsetsCompat ->
132 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
133 view.setPadding(
134 insets.left,
135 insets.top,
136 insets.right,
137 insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation)
138 )
139 windowInsets
140 }
141
142 private val getGamesDirectory =
143 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
144 if (result == null)
145 return@registerForActivityResult
146
147 val takeFlags =
148 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
149 requireActivity().contentResolver.takePersistableUriPermission(
150 result,
151 takeFlags
152 )
153
154 // When a new directory is picked, we currently will reset the existing games
155 // database. This effectively means that only one game directory is supported.
156 PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
157 .putString(GameHelper.KEY_GAME_PATH, result.toString())
158 .apply()
159
160 gamesViewModel.reloadGames(true)
161 }
162
163 private val getProdKey =
164 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
165 if (result == null)
166 return@registerForActivityResult
167
168 val takeFlags =
169 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
170 requireActivity().contentResolver.takePersistableUriPermission(
171 result,
172 takeFlags
173 )
174
175 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
176 if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) {
177 if (NativeLibrary.reloadKeys()) {
178 Toast.makeText(
179 requireContext(),
180 R.string.install_keys_success,
181 Toast.LENGTH_SHORT
182 ).show()
183 gamesViewModel.reloadGames(true)
184 } else {
185 Toast.makeText(
186 requireContext(),
187 R.string.install_keys_failure,
188 Toast.LENGTH_LONG
189 ).show()
190 }
191 }
192 }
193
194 private val getAmiiboKey =
195 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
196 if (result == null)
197 return@registerForActivityResult
198
199 val takeFlags =
200 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
201 requireActivity().contentResolver.takePersistableUriPermission(
202 result,
203 takeFlags
204 )
205
206 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
207 if (FileUtil.copyUriToInternalStorage(
208 requireContext(),
209 result,
210 dstPath,
211 "key_retail.bin"
212 )
213 ) {
214 if (NativeLibrary.reloadKeys()) {
215 Toast.makeText(
216 requireContext(),
217 R.string.install_keys_success,
218 Toast.LENGTH_SHORT
219 ).show()
220 } else {
221 Toast.makeText(
222 requireContext(),
223 R.string.install_amiibo_keys_failure,
224 Toast.LENGTH_LONG
225 ).show()
226 }
227 }
228 }
229
230 private val getDriver =
231 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
232 if (result == null)
233 return@registerForActivityResult
234
235 val takeFlags =
236 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
237 requireActivity().contentResolver.takePersistableUriPermission(
238 result,
239 takeFlags
240 )
241
242 val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
243 progressBinding.progressBar.isIndeterminate = true
244 val installationDialog = MaterialAlertDialogBuilder(requireContext())
245 .setTitle(R.string.installing_driver)
246 .setView(progressBinding.root)
247 .show()
248
249 lifecycleScope.launch {
250 withContext(Dispatchers.IO) {
251 // Ignore file exceptions when a user selects an invalid zip
252 try {
253 GpuDriverHelper.installCustomDriver(requireContext(), result)
254 } catch (_: IOException) {
255 }
256
257 withContext(Dispatchers.Main) {
258 installationDialog.dismiss()
259
260 val driverName = GpuDriverHelper.customDriverName
261 if (driverName != null) {
262 Toast.makeText(
263 requireContext(),
264 getString(
265 R.string.select_gpu_driver_install_success,
266 driverName
267 ),
268 Toast.LENGTH_SHORT
269 ).show()
270 } else {
271 Toast.makeText(
272 requireContext(),
273 R.string.select_gpu_driver_error,
274 Toast.LENGTH_LONG
275 ).show()
276 }
277 }
278 }
279 }
280 }
281}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index fde99f1a2..709a5b976 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -1,18 +1,58 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
1package org.yuzu.yuzu_emu.model 4package org.yuzu.yuzu_emu.model
2 5
3import androidx.lifecycle.LiveData 6import androidx.lifecycle.LiveData
4import androidx.lifecycle.MutableLiveData 7import androidx.lifecycle.MutableLiveData
5import androidx.lifecycle.ViewModel 8import androidx.lifecycle.ViewModel
9import androidx.lifecycle.viewModelScope
10import kotlinx.coroutines.Dispatchers
11import kotlinx.coroutines.launch
12import kotlinx.coroutines.withContext
13import org.yuzu.yuzu_emu.NativeLibrary
14import org.yuzu.yuzu_emu.utils.GameHelper
6 15
7class GamesViewModel : ViewModel() { 16class GamesViewModel : ViewModel() {
8 private val _games = MutableLiveData<ArrayList<Game>>() 17 private val _games = MutableLiveData<List<Game>>(emptyList())
9 val games: LiveData<ArrayList<Game>> get() = _games 18 val games: LiveData<List<Game>> get() = _games
19
20 private val _searchedGames = MutableLiveData<List<Game>>(emptyList())
21 val searchedGames: LiveData<List<Game>> get() = _searchedGames
22
23 private val _isReloading = MutableLiveData(false)
24 val isReloading: LiveData<Boolean> get() = _isReloading
25
26 private val _shouldSwapData = MutableLiveData(false)
27 val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData
10 28
11 init { 29 init {
12 _games.value = ArrayList() 30 reloadGames(false)
31 }
32
33 fun setSearchedGames(games: List<Game>) {
34 _searchedGames.postValue(games)
35 }
36
37 fun setShouldSwapData(shouldSwap: Boolean) {
38 _shouldSwapData.postValue(shouldSwap)
13 } 39 }
14 40
15 fun setGames(games: ArrayList<Game>) { 41 fun reloadGames(directoryChanged: Boolean) {
16 _games.value = games 42 if (isReloading.value == true)
43 return
44 _isReloading.postValue(true)
45
46 viewModelScope.launch {
47 withContext(Dispatchers.IO) {
48 NativeLibrary.resetRomMetadata()
49 _games.postValue(GameHelper.getGames())
50 _isReloading.postValue(false)
51
52 if (directoryChanged) {
53 setShouldSwapData(true)
54 }
55 }
56 }
17 } 57 }
18} 58}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt
new file mode 100644
index 000000000..c995ff12c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt
@@ -0,0 +1,11 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6data class HomeOption(
7 val titleId: Int,
8 val descriptionId: Int,
9 val iconId: Int,
10 val onClick: () -> Unit
11)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
new file mode 100644
index 000000000..74f12429c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -0,0 +1,17 @@
1package org.yuzu.yuzu_emu.model
2
3import androidx.lifecycle.LiveData
4import androidx.lifecycle.MutableLiveData
5import androidx.lifecycle.ViewModel
6
7class HomeViewModel : ViewModel() {
8 private val _navigationVisible = MutableLiveData(true)
9 val navigationVisible: LiveData<Boolean> get() = _navigationVisible
10
11 fun setNavigationVisible(visible: Boolean) {
12 if (_navigationVisible.value == visible) {
13 return
14 }
15 _navigationVisible.value = visible
16 }
17}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
new file mode 100644
index 000000000..0c609798b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -0,0 +1,220 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui
5
6import android.os.Bundle
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import androidx.activity.OnBackPressedCallback
11import androidx.appcompat.app.AppCompatActivity
12import androidx.core.content.ContextCompat
13import androidx.core.view.ViewCompat
14import androidx.core.view.WindowInsetsCompat
15import androidx.core.view.updatePadding
16import androidx.core.widget.doOnTextChanged
17import androidx.fragment.app.Fragment
18import androidx.fragment.app.activityViewModels
19import com.google.android.material.color.MaterialColors
20import com.google.android.material.search.SearchView
21import com.google.android.material.search.SearchView.TransitionState
22import info.debatty.java.stringsimilarity.Jaccard
23import org.yuzu.yuzu_emu.R
24import org.yuzu.yuzu_emu.adapters.GameAdapter
25import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
26import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
27import org.yuzu.yuzu_emu.model.Game
28import org.yuzu.yuzu_emu.model.GamesViewModel
29import org.yuzu.yuzu_emu.model.HomeViewModel
30import org.yuzu.yuzu_emu.utils.ThemeHelper
31import java.util.Locale
32
33class GamesFragment : Fragment() {
34 private var _binding: FragmentGamesBinding? = null
35 private val binding get() = _binding!!
36
37 private val gamesViewModel: GamesViewModel by activityViewModels()
38 private val homeViewModel: HomeViewModel by activityViewModels()
39
40 override fun onCreateView(
41 inflater: LayoutInflater,
42 container: ViewGroup?,
43 savedInstanceState: Bundle?
44 ): View {
45 _binding = FragmentGamesBinding.inflate(inflater)
46 return binding.root
47 }
48
49 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
50 // Use custom back navigation so the user doesn't back out of the app when trying to back
51 // out of the search view
52 requireActivity().onBackPressedDispatcher.addCallback(
53 viewLifecycleOwner,
54 object : OnBackPressedCallback(true) {
55 override fun handleOnBackPressed() {
56 if (binding.searchView.currentTransitionState == TransitionState.SHOWN) {
57 binding.searchView.hide()
58 } else {
59 requireActivity().finish()
60 }
61 }
62 })
63
64 binding.gridGames.apply {
65 layoutManager = AutofitGridLayoutManager(
66 requireContext(),
67 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
68 )
69 adapter = GameAdapter(requireActivity() as AppCompatActivity)
70 }
71 setUpSearch()
72
73 // Add swipe down to refresh gesture
74 binding.swipeRefresh.setOnRefreshListener {
75 gamesViewModel.reloadGames(false)
76 }
77
78 // Set theme color to the refresh animation's background
79 binding.swipeRefresh.setProgressBackgroundColorSchemeColor(
80 MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary)
81 )
82 binding.swipeRefresh.setColorSchemeColors(
83 MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary)
84 )
85
86 // Watch for when we get updates to any of our games lists
87 gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading ->
88 binding.swipeRefresh.isRefreshing = isReloading
89
90 if (!isReloading) {
91 if (gamesViewModel.games.value!!.isEmpty()) {
92 binding.noticeText.visibility = View.VISIBLE
93 } else {
94 binding.noticeText.visibility = View.GONE
95 }
96 }
97 }
98 gamesViewModel.games.observe(viewLifecycleOwner) {
99 (binding.gridGames.adapter as GameAdapter).submitList(it)
100 }
101 gamesViewModel.searchedGames.observe(viewLifecycleOwner) {
102 (binding.gridSearch.adapter as GameAdapter).submitList(it)
103 }
104 gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
105 if (shouldSwapData) {
106 (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
107 gamesViewModel.setShouldSwapData(false)
108 }
109 }
110
111 // Hide bottom navigation and FAB when using the search view
112 binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState ->
113 when (newState) {
114 TransitionState.SHOWING,
115 TransitionState.SHOWN -> {
116 (binding.gridSearch.adapter as GameAdapter).submitList(emptyList())
117 searchShown()
118 }
119 TransitionState.HIDDEN,
120 TransitionState.HIDING -> {
121 gamesViewModel.setSearchedGames(emptyList())
122 searchHidden()
123 }
124 }
125 }
126
127 // Ensure that bottom navigation or FAB don't appear upon recreation
128 val searchState = binding.searchView.currentTransitionState
129 if (searchState == TransitionState.SHOWN) {
130 searchShown()
131 } else if (searchState == TransitionState.HIDDEN) {
132 searchHidden()
133 }
134
135 setInsets()
136
137 // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn
138 binding.swipeRefresh.post {
139 binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!!
140 }
141 }
142
143 override fun onDestroyView() {
144 super.onDestroyView()
145 _binding = null
146 }
147
148 private fun searchShown() {
149 homeViewModel.setNavigationVisible(false)
150 requireActivity().window.statusBarColor =
151 ContextCompat.getColor(requireContext(), android.R.color.transparent)
152 }
153
154 private fun searchHidden() {
155 homeViewModel.setNavigationVisible(true)
156 requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity(
157 MaterialColors.getColor(
158 binding.root,
159 R.attr.colorSurface
160 ), ThemeHelper.SYSTEM_BAR_ALPHA
161 )
162 }
163
164 private inner class ScoredGame(val score: Double, val item: Game)
165
166 private fun setUpSearch() {
167 binding.gridSearch.apply {
168 layoutManager = AutofitGridLayoutManager(
169 requireContext(),
170 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
171 )
172 adapter = GameAdapter(requireActivity() as AppCompatActivity)
173 }
174
175 binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
176 val searchTerm = text.toString().lowercase(Locale.getDefault())
177 val searchAlgorithm = Jaccard(2)
178 val sortedList: List<Game> = gamesViewModel.games.value!!.mapNotNull { game ->
179 val title = game.title.lowercase(Locale.getDefault())
180 val score = searchAlgorithm.similarity(searchTerm, title)
181 if (score > 0.03) {
182 ScoredGame(score, game)
183 } else {
184 null
185 }
186 }.sortedByDescending { it.score }.map { it.item }
187 gamesViewModel.setSearchedGames(sortedList)
188 }
189 }
190
191 private fun setInsets() =
192 ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat ->
193 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
194 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
195
196 view.setPadding(
197 insets.left,
198 insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search),
199 insets.right,
200 insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
201 )
202 binding.gridSearch.updatePadding(
203 left = insets.left,
204 top = extraListSpacing,
205 right = insets.right,
206 bottom = insets.bottom + extraListSpacing
207 )
208
209 binding.swipeRefresh.setSlingshotDistance(
210 resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot)
211 )
212 binding.swipeRefresh.setProgressViewOffset(
213 false,
214 insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start),
215 insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
216 )
217
218 windowInsets
219 }
220}
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 69a371947..a16ca8529 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
@@ -3,42 +3,31 @@
3 3
4package org.yuzu.yuzu_emu.ui.main 4package org.yuzu.yuzu_emu.ui.main
5 5
6import android.content.DialogInterface
7import android.content.Intent
8import android.os.Bundle 6import android.os.Bundle
9import android.view.Menu
10import android.view.MenuItem
11import android.view.View 7import android.view.View
12import android.widget.Toast 8import android.view.ViewGroup.MarginLayoutParams
13import androidx.activity.result.contract.ActivityResultContracts 9import android.view.animation.PathInterpolator
10import androidx.activity.viewModels
14import androidx.appcompat.app.AppCompatActivity 11import androidx.appcompat.app.AppCompatActivity
15import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 12import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
16import androidx.core.view.ViewCompat 13import androidx.core.view.ViewCompat
17import androidx.core.view.WindowCompat 14import androidx.core.view.WindowCompat
18import androidx.core.view.WindowInsetsCompat 15import androidx.core.view.WindowInsetsCompat
19import androidx.core.view.updatePadding 16import androidx.navigation.fragment.NavHostFragment
20import androidx.lifecycle.lifecycleScope 17import androidx.navigation.ui.setupWithNavController
21import androidx.preference.PreferenceManager 18import com.google.android.material.color.MaterialColors
22import com.google.android.material.dialog.MaterialAlertDialogBuilder 19import com.google.android.material.elevation.ElevationOverlayProvider
23import kotlinx.coroutines.Dispatchers
24import kotlinx.coroutines.launch
25import kotlinx.coroutines.withContext
26import org.yuzu.yuzu_emu.NativeLibrary
27import org.yuzu.yuzu_emu.R 20import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.activities.EmulationActivity 21import org.yuzu.yuzu_emu.activities.EmulationActivity
29import org.yuzu.yuzu_emu.databinding.ActivityMainBinding 22import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
30import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 23import org.yuzu.yuzu_emu.model.HomeViewModel
31import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
32import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment
33import org.yuzu.yuzu_emu.utils.* 24import org.yuzu.yuzu_emu.utils.*
34import java.io.IOException
35
36class MainActivity : AppCompatActivity(), MainView {
37 private var platformGamesFragment: PlatformGamesFragment? = null
38 private val presenter = MainPresenter(this)
39 25
26class MainActivity : AppCompatActivity() {
40 private lateinit var binding: ActivityMainBinding 27 private lateinit var binding: ActivityMainBinding
41 28
29 private val homeViewModel: HomeViewModel by viewModels()
30
42 override fun onCreate(savedInstanceState: Bundle?) { 31 override fun onCreate(savedInstanceState: Bundle?) {
43 val splashScreen = installSplashScreen() 32 val splashScreen = installSplashScreen()
44 splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } 33 splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@@ -52,19 +41,36 @@ class MainActivity : AppCompatActivity(), MainView {
52 41
53 WindowCompat.setDecorFitsSystemWindows(window, false) 42 WindowCompat.setDecorFitsSystemWindows(window, false)
54 43
55 setSupportActionBar(binding.toolbarMain) 44 ThemeHelper.setNavigationBarColor(
56 presenter.onCreate() 45 this,
57 if (savedInstanceState == null) { 46 ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay(
58 StartupHandler.handleInit(this) 47 MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface),
59 platformGamesFragment = PlatformGamesFragment() 48 binding.navigationBar.elevation
60 supportFragmentManager.beginTransaction() 49 )
61 .add(R.id.games_platform_frame, platformGamesFragment!!) 50 )
62 .commit() 51
63 } else { 52 // Set up a central host fragment that is controlled via bottom navigation with xml navigation
64 platformGamesFragment = supportFragmentManager.getFragment( 53 val navHostFragment =
65 savedInstanceState, 54 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
66 PlatformGamesFragment.TAG 55 binding.navigationBar.setupWithNavController(navHostFragment.navController)
67 ) as PlatformGamesFragment? 56
57 binding.statusBarShade.setBackgroundColor(
58 ThemeHelper.getColorWithOpacity(
59 MaterialColors.getColor(
60 binding.root,
61 R.attr.colorSurface
62 ), ThemeHelper.SYSTEM_BAR_ALPHA
63 )
64 )
65
66 // Prevents navigation from being drawn for a short time on recreation if set to hidden
67 if (homeViewModel.navigationVisible.value == false) {
68 binding.navigationBar.visibility = View.INVISIBLE
69 binding.statusBarShade.visibility = View.INVISIBLE
70 }
71
72 homeViewModel.navigationVisible.observe(this) { visible ->
73 showNavigation(visible)
68 } 74 }
69 75
70 // Dismiss previous notifications (should not happen unless a crash occurred) 76 // Dismiss previous notifications (should not happen unless a crash occurred)
@@ -73,78 +79,24 @@ class MainActivity : AppCompatActivity(), MainView {
73 setInsets() 79 setInsets()
74 } 80 }
75 81
76 override fun onSaveInstanceState(outState: Bundle) { 82 private fun showNavigation(visible: Boolean) {
77 super.onSaveInstanceState(outState) 83 binding.navigationBar.animate().apply {
78 supportFragmentManager.putFragment( 84 if (visible) {
79 outState, 85 binding.navigationBar.visibility = View.VISIBLE
80 PlatformGamesFragment.TAG, 86 binding.navigationBar.translationY = binding.navigationBar.height.toFloat() * 2
81 platformGamesFragment!! 87 duration = 300
82 ) 88 translationY(0f)
83 } 89 interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
84 90 } else {
85 override fun onCreateOptionsMenu(menu: Menu): Boolean { 91 duration = 300
86 menuInflater.inflate(R.menu.menu_game_grid, menu) 92 translationY(binding.navigationBar.height.toFloat() * 2)
87 return true 93 interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
88 }
89
90 /**
91 * MainView
92 */
93 override fun setVersionString(version: String) {
94 binding.toolbarMain.subtitle = version
95 }
96
97 override fun launchSettingsActivity(menuTag: String) {
98 SettingsActivity.launch(this, menuTag, "")
99 }
100
101 override fun launchFileListActivity(request: Int) {
102 when (request) {
103 MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
104 MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*"))
105 MainPresenter.REQUEST_INSTALL_AMIIBO_KEYS -> getAmiiboKey.launch(arrayOf("*/*"))
106 MainPresenter.REQUEST_SELECT_GPU_DRIVER -> {
107 // Get the driver name for the dialog message.
108 var driverName = GpuDriverHelper.customDriverName
109 if (driverName == null) {
110 driverName = getString(R.string.system_gpu_driver)
111 }
112
113 MaterialAlertDialogBuilder(this)
114 .setTitle(getString(R.string.select_gpu_driver_title))
115 .setMessage(driverName)
116 .setNegativeButton(android.R.string.cancel, null)
117 .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
118 GpuDriverHelper.installDefaultDriver(this)
119 Toast.makeText(
120 this,
121 R.string.select_gpu_driver_use_default,
122 Toast.LENGTH_SHORT
123 ).show()
124 }
125 .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
126 getDriver.launch(arrayOf("application/zip"))
127 }
128 .show()
129 } 94 }
130 } 95 }.withEndAction {
131 } 96 if (!visible) {
132 97 binding.navigationBar.visibility = View.INVISIBLE
133 /** 98 }
134 * Called by the framework whenever any actionbar/toolbar icon is clicked. 99 }.start()
135 *
136 * @param item The icon that was clicked on.
137 * @return True if the event was handled, false to bubble it up to the OS.
138 */
139 override fun onOptionsItemSelected(item: MenuItem): Boolean {
140 return presenter.handleOptionSelection(item.itemId)
141 }
142
143 private fun refreshFragment() {
144 if (platformGamesFragment != null) {
145 NativeLibrary.resetRomMetadata()
146 platformGamesFragment!!.refresh()
147 }
148 } 100 }
149 101
150 override fun onDestroy() { 102 override fun onDestroy() {
@@ -152,145 +104,12 @@ class MainActivity : AppCompatActivity(), MainView {
152 super.onDestroy() 104 super.onDestroy()
153 } 105 }
154 106
155 private fun setInsets() { 107 private fun setInsets() =
156 ViewCompat.setOnApplyWindowInsetsListener(binding.gamesPlatformFrame) { view: View, windowInsets: WindowInsetsCompat -> 108 ViewCompat.setOnApplyWindowInsetsListener(binding.statusBarShade) { view: View, windowInsets: WindowInsetsCompat ->
157 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 109 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
158 view.updatePadding(left = insets.left, right = insets.right) 110 val mlpShade = view.layoutParams as MarginLayoutParams
159 InsetsHelper.insetAppBar(insets, binding.appbarMain) 111 mlpShade.height = insets.top
112 binding.statusBarShade.layoutParams = mlpShade
160 windowInsets 113 windowInsets
161 } 114 }
162 }
163
164 private val getGamesDirectory =
165 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
166 if (result == null)
167 return@registerForActivityResult
168
169 val takeFlags =
170 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
171 contentResolver.takePersistableUriPermission(
172 result,
173 takeFlags
174 )
175
176 // When a new directory is picked, we currently will reset the existing games
177 // database. This effectively means that only one game directory is supported.
178 PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
179 .putString(GameHelper.KEY_GAME_PATH, result.toString())
180 .apply()
181 }
182
183 private val getProdKey =
184 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
185 if (result == null)
186 return@registerForActivityResult
187
188 val takeFlags =
189 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
190 contentResolver.takePersistableUriPermission(
191 result,
192 takeFlags
193 )
194
195 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
196 if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "prod.keys")) {
197 if (NativeLibrary.reloadKeys()) {
198 Toast.makeText(
199 this,
200 R.string.install_keys_success,
201 Toast.LENGTH_SHORT
202 ).show()
203 refreshFragment()
204 } else {
205 Toast.makeText(
206 this,
207 R.string.install_keys_failure,
208 Toast.LENGTH_LONG
209 ).show()
210 }
211 }
212 }
213
214 private val getAmiiboKey =
215 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
216 if (result == null)
217 return@registerForActivityResult
218
219 val takeFlags =
220 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
221 contentResolver.takePersistableUriPermission(
222 result,
223 takeFlags
224 )
225
226 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
227 if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "key_retail.bin")) {
228 if (NativeLibrary.reloadKeys()) {
229 Toast.makeText(
230 this,
231 R.string.install_keys_success,
232 Toast.LENGTH_SHORT
233 ).show()
234 refreshFragment()
235 } else {
236 Toast.makeText(
237 this,
238 R.string.install_amiibo_keys_failure,
239 Toast.LENGTH_LONG
240 ).show()
241 }
242 }
243 }
244
245 private val getDriver =
246 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
247 if (result == null)
248 return@registerForActivityResult
249
250 val takeFlags =
251 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
252 contentResolver.takePersistableUriPermission(
253 result,
254 takeFlags
255 )
256
257 val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
258 progressBinding.progressBar.isIndeterminate = true
259 val installationDialog = MaterialAlertDialogBuilder(this)
260 .setTitle(R.string.installing_driver)
261 .setView(progressBinding.root)
262 .show()
263
264 lifecycleScope.launch {
265 withContext(Dispatchers.IO) {
266 // Ignore file exceptions when a user selects an invalid zip
267 try {
268 GpuDriverHelper.installCustomDriver(applicationContext, result)
269 } catch (_: IOException) {
270 }
271
272 withContext(Dispatchers.Main) {
273 installationDialog.dismiss()
274
275 val driverName = GpuDriverHelper.customDriverName
276 if (driverName != null) {
277 Toast.makeText(
278 applicationContext,
279 getString(
280 R.string.select_gpu_driver_install_success,
281 driverName
282 ),
283 Toast.LENGTH_SHORT
284 ).show()
285 } else {
286 Toast.makeText(
287 applicationContext,
288 R.string.select_gpu_driver_error,
289 Toast.LENGTH_LONG
290 ).show()
291 }
292 }
293 }
294 }
295 }
296} 115}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt
deleted file mode 100644
index a7ddc333f..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt
+++ /dev/null
@@ -1,52 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui.main
5
6import org.yuzu.yuzu_emu.BuildConfig
7import org.yuzu.yuzu_emu.R
8import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
9
10class MainPresenter(private val view: MainView) {
11 fun onCreate() {
12 val versionName = BuildConfig.VERSION_NAME
13 view.setVersionString(versionName)
14 }
15
16 private fun launchFileListActivity(request: Int) {
17 view.launchFileListActivity(request)
18 }
19
20 fun handleOptionSelection(itemId: Int): Boolean {
21 when (itemId) {
22 R.id.menu_settings_core -> {
23 view.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG)
24 return true
25 }
26 R.id.button_add_directory -> {
27 launchFileListActivity(REQUEST_ADD_DIRECTORY)
28 return true
29 }
30 R.id.button_install_keys -> {
31 launchFileListActivity(REQUEST_INSTALL_KEYS)
32 return true
33 }
34 R.id.button_install_amiibo_keys -> {
35 launchFileListActivity(REQUEST_INSTALL_AMIIBO_KEYS)
36 return true
37 }
38 R.id.button_select_gpu_driver -> {
39 launchFileListActivity(REQUEST_SELECT_GPU_DRIVER)
40 return true
41 }
42 }
43 return false
44 }
45
46 companion object {
47 const val REQUEST_ADD_DIRECTORY = 1
48 const val REQUEST_INSTALL_KEYS = 2
49 const val REQUEST_INSTALL_AMIIBO_KEYS = 3
50 const val REQUEST_SELECT_GPU_DRIVER = 4
51 }
52}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt
deleted file mode 100644
index 4dc9f0706..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt
+++ /dev/null
@@ -1,23 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui.main
5
6/**
7 * Abstraction for the screen that shows on application launch.
8 * Implementations will differ primarily to target touch-screen
9 * or non-touch screen devices.
10 */
11interface MainView {
12 /**
13 * Pass the view the native library's version string. Displaying
14 * it is optional.
15 *
16 * @param version A string pulled from native code.
17 */
18 fun setVersionString(version: String)
19
20 fun launchSettingsActivity(menuTag: String)
21
22 fun launchFileListActivity(request: Int)
23}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt
deleted file mode 100644
index 443a37cd2..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt
+++ /dev/null
@@ -1,109 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.ui.platform
5
6import android.os.Bundle
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import androidx.appcompat.app.AppCompatActivity
11import androidx.core.view.ViewCompat
12import androidx.core.view.WindowInsetsCompat
13import androidx.core.view.updatePadding
14import androidx.fragment.app.Fragment
15import androidx.lifecycle.ViewModelProvider
16import com.google.android.material.color.MaterialColors
17import org.yuzu.yuzu_emu.R
18import org.yuzu.yuzu_emu.adapters.GameAdapter
19import org.yuzu.yuzu_emu.databinding.FragmentGridBinding
20import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
21import org.yuzu.yuzu_emu.model.GamesViewModel
22import org.yuzu.yuzu_emu.utils.GameHelper
23
24class PlatformGamesFragment : Fragment() {
25 private var _binding: FragmentGridBinding? = null
26 private val binding get() = _binding!!
27
28 private lateinit var gamesViewModel: GamesViewModel
29
30 override fun onCreateView(
31 inflater: LayoutInflater,
32 container: ViewGroup?,
33 savedInstanceState: Bundle?
34 ): View {
35 _binding = FragmentGridBinding.inflate(inflater)
36 return binding.root
37 }
38
39 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
40 gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java]
41
42 binding.gridGames.apply {
43 layoutManager = AutofitGridLayoutManager(
44 requireContext(),
45 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
46 )
47 adapter =
48 GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!)
49 }
50
51 // Add swipe down to refresh gesture
52 binding.swipeRefresh.setOnRefreshListener {
53 refresh()
54 binding.swipeRefresh.isRefreshing = false
55 }
56
57 // Set theme color to the refresh animation's background
58 binding.swipeRefresh.setProgressBackgroundColorSchemeColor(
59 MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary)
60 )
61 binding.swipeRefresh.setColorSchemeColors(
62 MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary)
63 )
64
65 gamesViewModel.games.observe(viewLifecycleOwner) {
66 (binding.gridGames.adapter as GameAdapter).swapData(it)
67 updateTextView()
68 }
69
70 setInsets()
71
72 refresh()
73 }
74
75 override fun onResume() {
76 super.onResume()
77 refresh()
78 }
79
80 override fun onDestroyView() {
81 super.onDestroyView()
82 _binding = null
83 }
84
85 fun refresh() {
86 gamesViewModel.setGames(GameHelper.getGames())
87 updateTextView()
88 }
89
90 private fun updateTextView() {
91 if (_binding == null)
92 return
93
94 binding.gamelistEmptyText.visibility =
95 if ((binding.gridGames.adapter as GameAdapter).itemCount == 0) View.VISIBLE else View.GONE
96 }
97
98 private fun setInsets() {
99 ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat ->
100 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
101 view.updatePadding(bottom = insets.bottom)
102 windowInsets
103 }
104 }
105
106 companion object {
107 const val TAG = "PlatformGamesFragment"
108 }
109}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt
deleted file mode 100644
index e2e56eb06..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt
+++ /dev/null
@@ -1,48 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import androidx.preference.PreferenceManager
7import android.text.Html
8import android.text.method.LinkMovementMethod
9import android.view.View
10import android.widget.TextView
11import com.google.android.material.dialog.MaterialAlertDialogBuilder
12import org.yuzu.yuzu_emu.R
13import org.yuzu.yuzu_emu.YuzuApplication
14import org.yuzu.yuzu_emu.features.settings.model.Settings
15import org.yuzu.yuzu_emu.ui.main.MainActivity
16import org.yuzu.yuzu_emu.ui.main.MainPresenter
17
18object StartupHandler {
19 private val preferences =
20 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
21
22 private fun handleStartupPromptDismiss(parent: MainActivity) {
23 parent.launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS)
24 }
25
26 private fun markFirstBoot() {
27 preferences.edit()
28 .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
29 .apply()
30 }
31
32 fun handleInit(parent: MainActivity) {
33 if (preferences.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)) {
34 markFirstBoot()
35 val alert = MaterialAlertDialogBuilder(parent)
36 .setMessage(Html.fromHtml(parent.resources.getString(R.string.app_disclaimer)))
37 .setTitle(R.string.app_name)
38 .setIcon(R.drawable.ic_launcher)
39 .setPositiveButton(android.R.string.ok, null)
40 .setOnDismissListener {
41 handleStartupPromptDismiss(parent)
42 }
43 .show()
44 (alert.findViewById<View>(android.R.id.message) as TextView?)!!.movementMethod =
45 LinkMovementMethod.getInstance()
46 }
47 }
48}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt
index ce6396e91..481498f7b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt
@@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.R
15import kotlin.math.roundToInt 15import kotlin.math.roundToInt
16 16
17object ThemeHelper { 17object ThemeHelper {
18 private const val NAV_BAR_ALPHA = 0.9f 18 const val SYSTEM_BAR_ALPHA = 0.9f
19 19
20 @JvmStatic 20 @JvmStatic
21 fun setTheme(activity: AppCompatActivity) { 21 fun setTheme(activity: AppCompatActivity) {
@@ -29,10 +29,6 @@ object ThemeHelper {
29 windowController.isAppearanceLightNavigationBars = isLightMode 29 windowController.isAppearanceLightNavigationBars = isLightMode
30 30
31 activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.transparent) 31 activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.transparent)
32
33 val navigationBarColor =
34 MaterialColors.getColor(activity.window.decorView, R.attr.colorSurface)
35 setNavigationBarColor(activity, navigationBarColor)
36 } 32 }
37 33
38 @JvmStatic 34 @JvmStatic
@@ -48,7 +44,7 @@ object ThemeHelper {
48 } else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION || 44 } else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
49 gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION 45 gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION
50 ) { 46 ) {
51 activity.window.navigationBarColor = getColorWithOpacity(color, NAV_BAR_ALPHA) 47 activity.window.navigationBarColor = getColorWithOpacity(color, SYSTEM_BAR_ALPHA)
52 } else { 48 } else {
53 activity.window.navigationBarColor = ContextCompat.getColor( 49 activity.window.navigationBarColor = ContextCompat.getColor(
54 activity.applicationContext, 50 activity.applicationContext,
@@ -58,7 +54,7 @@ object ThemeHelper {
58 } 54 }
59 55
60 @ColorInt 56 @ColorInt
61 private fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { 57 fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
62 return Color.argb( 58 return Color.argb(
63 (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color), 59 (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color),
64 Color.green(color), Color.blue(color) 60 Color.green(color), Color.blue(color)
diff --git a/src/android/app/src/main/res/drawable/ic_add.xml b/src/android/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 000000000..f7deb2532
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_input.xml b/src/android/app/src/main/res/drawable/ic_input.xml
new file mode 100644
index 000000000..c170865ef
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_input.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:autoMirrored="true"
5 android:viewportWidth="24"
6 android:viewportHeight="24">
7 <path
8 android:fillColor="?attr/colorControlNormal"
9 android:pathData="M21,3.01H3c-1.1,0 -2,0.9 -2,2V9h2V4.99h18v14.03H3V15H1v4.01c0,1.1 0.9,1.98 2,1.98h18c1.1,0 2,-0.88 2,-1.98v-14c0,-1.11 -0.9,-2 -2,-2zM11,16l4,-4 -4,-4v3H1v2h10v3z" />
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_nfc.xml b/src/android/app/src/main/res/drawable/ic_nfc.xml
new file mode 100644
index 000000000..3dacf798b
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_nfc.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_options.xml b/src/android/app/src/main/res/drawable/ic_options.xml
new file mode 100644
index 000000000..91d52f1b8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_options.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 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" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_unlock.xml b/src/android/app/src/main/res/drawable/ic_unlock.xml
new file mode 100644
index 000000000..40952cbc5
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_unlock.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml b/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml
new file mode 100644
index 000000000..4400e9eaf
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml
@@ -0,0 +1,18 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="614.697dp"
3 android:height="683dp"
4 android:viewportWidth="614.4"
5 android:viewportHeight="682.67">
6 <group>
7 <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" />
8 <path
9 android:fillColor="?attr/colorPrimary"
10 android:pathData="M340.81,138V682.08c150.26,0 272.06,-121.81 272.06,-272.06S491.07,138 340.81,138M394,197.55a219.06,219.06 0,0 1,0 424.94V197.55" />
11 </group>
12 <group>
13 <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" />
14 <path
15 android:fillColor="?attr/colorPrimary"
16 android:pathData="M272.79,1.92C122.53,1.92 0.73,123.73 0.73,274s121.8,272.07 272.06,272.07ZM219.65,61.51v425A219,219 0,0 1,118 119.18,217.51 217.51,0 0,1 219.65,61.51" />
17 </group>
18</vector>
diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml
index 059aaa9b4..9002b0642 100644
--- a/src/android/app/src/main/res/layout/activity_main.xml
+++ b/src/android/app/src/main/res/layout/activity_main.xml
@@ -1,28 +1,32 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" 2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
4 android:id="@+id/coordinator_main" 6 android:id="@+id/coordinator_main"
5 android:layout_width="match_parent" 7 android:layout_width="match_parent"
6 android:layout_height="match_parent"> 8 android:layout_height="match_parent">
7 9
8 <com.google.android.material.appbar.AppBarLayout 10 <androidx.fragment.app.FragmentContainerView
9 android:id="@+id/appbar_main" 11 android:id="@+id/fragment_container"
10 android:layout_width="match_parent" 12 android:name="androidx.navigation.fragment.NavHostFragment"
11 android:layout_height="wrap_content" 13 android:layout_width="0dp"
12 android:fitsSystemWindows="true" 14 android:layout_height="0dp"
13 app:liftOnScrollTargetViewId="@id/grid_games"> 15 app:defaultNavHost="true"
14 16 app:layout_constraintBottom_toBottomOf="parent"
15 <androidx.appcompat.widget.Toolbar 17 app:layout_constraintLeft_toLeftOf="parent"
16 android:id="@+id/toolbar_main" 18 app:layout_constraintRight_toRightOf="parent"
17 android:layout_width="match_parent" 19 app:layout_constraintTop_toTopOf="parent"
18 android:layout_height="?attr/actionBarSize" /> 20 app:navGraph="@navigation/home_navigation"
21 tools:layout="@layout/fragment_games" />
19 22
20 </com.google.android.material.appbar.AppBarLayout> 23 <com.google.android.material.bottomnavigation.BottomNavigationView
21 24 android:id="@+id/navigation_bar"
22 <FrameLayout
23 android:id="@+id/games_platform_frame"
24 android:layout_width="match_parent" 25 android:layout_width="match_parent"
25 android:layout_height="match_parent" 26 android:layout_height="wrap_content"
26 app:layout_behavior="@string/appbar_scrolling_view_behavior" /> 27 app:layout_constraintBottom_toBottomOf="parent"
28 app:layout_constraintLeft_toLeftOf="parent"
29 app:layout_constraintRight_toRightOf="parent"
30 app:menu="@menu/menu_navigation" />
27 31
28</androidx.coordinatorlayout.widget.CoordinatorLayout> 32</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/card_home_option.xml b/src/android/app/src/main/res/layout/card_home_option.xml
new file mode 100644
index 000000000..aea354783
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_home_option.xml
@@ -0,0 +1,53 @@
1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 style="?attr/materialCardViewFilledStyle"
6 android:id="@+id/option_card"
7 android:layout_width="match_parent"
8 android:layout_height="wrap_content"
9 android:layout_marginVertical="8dp"
10 android:layout_marginHorizontal="16dp"
11 android:background="?attr/selectableItemBackground"
12 android:clickable="true"
13 android:focusable="true">
14
15 <LinearLayout
16 android:layout_width="match_parent"
17 android:layout_height="wrap_content">
18
19 <ImageView
20 android:id="@+id/option_icon"
21 android:layout_width="24dp"
22 android:layout_height="24dp"
23 android:layout_marginStart="28dp"
24 android:layout_gravity="center_vertical"
25 app:tint="?attr/colorPrimary" />
26
27 <LinearLayout
28 android:layout_width="match_parent"
29 android:layout_height="wrap_content"
30 android:layout_margin="16dp"
31 android:orientation="vertical">
32
33 <com.google.android.material.textview.MaterialTextView
34 style="@style/TextAppearance.Material3.BodyMedium"
35 android:id="@+id/option_title"
36 android:layout_width="match_parent"
37 android:layout_height="wrap_content"
38 android:textAlignment="viewStart"
39 tools:text="@string/install_prod_keys" />
40
41 <com.google.android.material.textview.MaterialTextView
42 style="@style/TextAppearance.Material3.BodySmall"
43 android:id="@+id/option_description"
44 android:layout_width="match_parent"
45 android:layout_height="wrap_content"
46 android:textAlignment="viewStart"
47 tools:text="@string/install_prod_keys_description" />
48
49 </LinearLayout>
50
51 </LinearLayout>
52
53</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml
new file mode 100644
index 000000000..5cfe76de3
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_games.xml
@@ -0,0 +1,80 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/coordinator_main"
7 android:layout_width="match_parent"
8 android:layout_height="match_parent"
9 android:background="?attr/colorSurface">
10
11 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
12 android:id="@+id/swipe_refresh"
13 android:layout_width="match_parent"
14 android:layout_height="match_parent"
15 android:clipToPadding="false"
16 app:layout_behavior="@string/searchbar_scrolling_view_behavior">
17
18 <RelativeLayout
19 android:layout_width="match_parent"
20 android:layout_height="match_parent">
21
22 <com.google.android.material.textview.MaterialTextView
23 android:id="@+id/notice_text"
24 style="@style/TextAppearance.Material3.BodyLarge"
25 android:layout_width="match_parent"
26 android:layout_height="match_parent"
27 android:gravity="center"
28 android:padding="@dimen/spacing_large"
29 android:text="@string/empty_gamelist"
30 tools:visibility="gone" />
31
32 <androidx.recyclerview.widget.RecyclerView
33 android:id="@+id/grid_games"
34 android:layout_width="match_parent"
35 android:layout_height="match_parent"
36 android:clipToPadding="false"
37 tools:listitem="@layout/card_game" />
38
39 </RelativeLayout>
40
41 </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
42
43 <com.google.android.material.appbar.AppBarLayout
44 android:id="@+id/app_bar_search"
45 android:layout_width="match_parent"
46 android:layout_height="wrap_content"
47 app:liftOnScrollTargetViewId="@id/grid_games">
48
49 <FrameLayout
50 android:layout_width="match_parent"
51 android:layout_height="wrap_content"
52 android:fitsSystemWindows="true">
53
54 <com.google.android.material.search.SearchBar
55 android:id="@+id/search_bar"
56 android:layout_width="match_parent"
57 android:layout_height="wrap_content"
58 android:hint="@string/home_search_games" />
59
60 </FrameLayout>
61
62 </com.google.android.material.appbar.AppBarLayout>
63
64 <com.google.android.material.search.SearchView
65 android:id="@+id/search_view"
66 android:layout_width="match_parent"
67 android:layout_height="match_parent"
68 android:hint="@string/home_search_games"
69 app:layout_anchor="@id/search_bar">
70
71 <androidx.recyclerview.widget.RecyclerView
72 android:id="@+id/grid_search"
73 android:layout_width="match_parent"
74 android:layout_height="match_parent"
75 android:clipToPadding="false"
76 tools:listitem="@layout/card_game" />
77
78 </com.google.android.material.search.SearchView>
79
80</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_grid.xml b/src/android/app/src/main/res/layout/fragment_grid.xml
deleted file mode 100644
index bfb670b6d..000000000
--- a/src/android/app/src/main/res/layout/fragment_grid.xml
+++ /dev/null
@@ -1,37 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent">
7
8 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
9 android:id="@+id/swipe_refresh"
10 android:layout_width="match_parent"
11 android:layout_height="match_parent">
12
13 <RelativeLayout
14 android:layout_width="match_parent"
15 android:layout_height="match_parent">
16
17 <TextView
18 android:id="@+id/gamelist_empty_text"
19 android:layout_width="match_parent"
20 android:layout_height="match_parent"
21 android:gravity="center"
22 android:text="@string/empty_gamelist"
23 android:textSize="18sp"
24 android:visibility="gone" />
25
26 <androidx.recyclerview.widget.RecyclerView
27 android:id="@+id/grid_games"
28 android:layout_width="match_parent"
29 android:layout_height="match_parent"
30 android:clipToPadding="false"
31 tools:listitem="@layout/card_game" />
32
33 </RelativeLayout>
34
35 </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
36
37</FrameLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_options.xml b/src/android/app/src/main/res/layout/fragment_options.xml
new file mode 100644
index 000000000..ec6e7c205
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_options.xml
@@ -0,0 +1,30 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.core.widget.NestedScrollView
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 android:id="@+id/scroll_view_options"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="?attr/colorSurface"
8 android:clipToPadding="false">
9
10 <androidx.appcompat.widget.LinearLayoutCompat
11 android:layout_width="match_parent"
12 android:layout_height="match_parent"
13 android:orientation="vertical"
14 android:background="?attr/colorSurface">
15
16 <ImageView
17 android:layout_width="128dp"
18 android:layout_height="128dp"
19 android:layout_margin="64dp"
20 android:layout_gravity="center_horizontal"
21 android:src="@drawable/ic_yuzu_themed" />
22
23 <androidx.recyclerview.widget.RecyclerView
24 android:id="@+id/options_list"
25 android:layout_width="match_parent"
26 android:layout_height="match_parent" />
27
28 </androidx.appcompat.widget.LinearLayoutCompat>
29
30</androidx.core.widget.NestedScrollView>
diff --git a/src/android/app/src/main/res/menu/menu_game_grid.xml b/src/android/app/src/main/res/menu/menu_game_grid.xml
deleted file mode 100644
index 73046de0e..000000000
--- a/src/android/app/src/main/res/menu/menu_game_grid.xml
+++ /dev/null
@@ -1,47 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto">
4
5 <item
6 android:id="@+id/button_file_menu"
7 android:icon="@drawable/ic_folder"
8 android:title="@string/select_game_folder"
9 app:showAsAction="ifRoom">
10
11 <menu>
12
13 <item
14 android:id="@+id/button_add_directory"
15 android:icon="@drawable/ic_folder"
16 android:title="@string/select_game_folder"
17 app:showAsAction="ifRoom" />
18
19 <item
20 android:id="@+id/button_install_keys"
21 android:icon="@drawable/ic_install"
22 android:title="@string/install_keys"
23 app:showAsAction="ifRoom" />
24
25 <item
26 android:id="@+id/button_install_amiibo_keys"
27 android:icon="@drawable/ic_install"
28 android:title="@string/install_amiibo_keys"
29 app:showAsAction="ifRoom" />
30
31 <item
32 android:id="@+id/button_select_gpu_driver"
33 android:icon="@drawable/ic_settings"
34 android:title="@string/select_gpu_driver"
35 app:showAsAction="ifRoom" />
36
37 </menu>
38
39 </item>
40
41 <item
42 android:id="@+id/menu_settings_core"
43 android:icon="@drawable/ic_settings"
44 android:title="@string/grid_menu_core_settings"
45 app:showAsAction="ifRoom" />
46
47</menu>
diff --git a/src/android/app/src/main/res/menu/menu_navigation.xml b/src/android/app/src/main/res/menu/menu_navigation.xml
new file mode 100644
index 000000000..ca5a656a6
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_navigation.xml
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <item
5 android:id="@+id/gamesFragment"
6 android:icon="@drawable/ic_controller"
7 android:title="@string/home_games" />
8
9 <item
10 android:id="@+id/optionsFragment"
11 android:icon="@drawable/ic_options"
12 android:title="@string/home_options" />
13
14</menu>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
new file mode 100644
index 000000000..e85e24a85
--- /dev/null
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -0,0 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?>
2<navigation xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/home_navigation"
5 app:startDestination="@id/gamesFragment">
6
7 <fragment
8 android:id="@+id/gamesFragment"
9 android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
10 android:label="PlatformGamesFragment" />
11
12 <fragment
13 android:id="@+id/optionsFragment"
14 android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment"
15 android:label="OptionsFragment" />
16
17</navigation>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index db0a8f7e5..23977c9f1 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -1,10 +1,15 @@
1<resources> 1<resources>
2 <dimen name="spacing_small">4dp</dimen> 2 <dimen name="spacing_small">4dp</dimen>
3 <dimen name="spacing_med">8dp</dimen>
3 <dimen name="spacing_medlarge">12dp</dimen> 4 <dimen name="spacing_medlarge">12dp</dimen>
4 <dimen name="spacing_large">16dp</dimen> 5 <dimen name="spacing_large">16dp</dimen>
5 <dimen name="spacing_xtralarge">32dp</dimen> 6 <dimen name="spacing_xtralarge">32dp</dimen>
6 <dimen name="spacing_list">64dp</dimen> 7 <dimen name="spacing_list">64dp</dimen>
7 <dimen name="spacing_fab">72dp</dimen> 8 <dimen name="spacing_navigation">80dp</dimen>
9 <dimen name="spacing_search">88dp</dimen>
10 <dimen name="spacing_refresh_slingshot">80dp</dimen>
11 <dimen name="spacing_refresh_start">32dp</dimen>
12 <dimen name="spacing_refresh_end">96dp</dimen>
8 <dimen name="menu_width">256dp</dimen> 13 <dimen name="menu_width">256dp</dimen>
9 <dimen name="card_width">160dp</dimen> 14 <dimen name="card_width">160dp</dimen>
10 15
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 75d1f2293..564bad081 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -9,6 +9,24 @@
9 <string name="app_notification_channel_description">yuzu Switch emulator notifications</string> 9 <string name="app_notification_channel_description">yuzu Switch emulator notifications</string>
10 <string name="app_notification_running">yuzu is running</string> 10 <string name="app_notification_running">yuzu is running</string>
11 11
12 <!-- Home strings -->
13 <string name="home_games">Games</string>
14 <string name="home_options">Options</string>
15 <string name="add_games">Add Games</string>
16 <string name="add_games_description">Select your games folder</string>
17 <string name="home_search_games">Search Games</string>
18 <string name="install_prod_keys">Install Prod.keys</string>
19 <string name="install_prod_keys_description">Required to decrypt retail games</string>
20 <string name="install_amiibo_keys">Install Amiibo Keys</string>
21 <string name="install_amiibo_keys_description">Required to use Amiibo in game</string>
22 <string name="install_keys_success">Keys successfully installed</string>
23 <string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
24 <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string>
25 <string name="install_gpu_driver">Install GPU Driver</string>
26 <string name="install_gpu_driver_description">Use a different driver for potentially better performance or accuracy</string>
27 <string name="settings">Settings</string>
28 <string name="settings_description">Configure emulator settings</string>
29
12 <!-- General settings strings --> 30 <!-- General settings strings -->
13 <string name="frame_limit_enable">Enable limit speed</string> 31 <string name="frame_limit_enable">Enable limit speed</string>
14 <string name="frame_limit_enable_description">When enabled, emulation speed will be limited to a specified percentage of normal speed.</string> 32 <string name="frame_limit_enable_description">When enabled, emulation speed will be limited to a specified percentage of normal speed.</string>
@@ -51,17 +69,6 @@
51 <string name="error_saving">Error saving %1$s.ini: %2$s</string> 69 <string name="error_saving">Error saving %1$s.ini: %2$s</string>
52 <string name="loading">Loading...</string> 70 <string name="loading">Loading...</string>
53 71
54 <!-- Game Grid Screen-->
55 <string name="grid_menu_core_settings">Settings</string>
56
57 <!-- Add Directory Screen-->
58 <string name="select_game_folder">Select game folder</string>
59 <string name="install_keys">Install keys</string>
60 <string name="install_amiibo_keys">Install amiibo keys</string>
61 <string name="install_keys_success">Keys successfully installed</string>
62 <string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
63 <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string>
64
65 <!-- GPU driver installation --> 72 <!-- GPU driver installation -->
66 <string name="select_gpu_driver">Select GPU driver</string> 73 <string name="select_gpu_driver">Select GPU driver</string>
67 <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string> 74 <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>