summaryrefslogtreecommitdiffstats
path: root/src/android/app
diff options
context:
space:
mode:
authorCharles Lombardo <clombardo169@gmail.com>2023-04-06 02:26:53 +0200
committerbunnei <bunneidev@gmail.com>2023-06-03 09:05:51 +0200
commit233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38 (patch)
treea9e82ac5af26b935ca0d5aeb9e7be9f780667369 /src/android/app
parentandroid: Enforce Vulkan 1.1 support as minimum (diff)
downloadyuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.gz
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.bz2
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.lz
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.xz
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.zst
yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.zip
Diffstat (limited to 'src/android/app')
-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 {
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.5.3")
+ implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
+ implementation("info.debatty:java-string-similarity:2.0.0")
}
fun 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
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider.OnChangeListener
@@ -202,7 +201,7 @@ open class EmulationActivity : AppCompatActivity() {
private const val EMULATION_RUNNING_NOTIFICATION = 0x1000
@JvmStatic
- fun launch(activity: FragmentActivity, game: Game) {
+ fun launch(activity: AppCompatActivity, game: Game) {
val launcher = Intent(activity, EmulationActivity::class.java)
launcher.putExtra(EXTRA_SELECTED_GAME, game)
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 @@
package org.yuzu.yuzu_emu.adapters
+import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.LayoutInflater
@@ -11,29 +12,25 @@ import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardGameBinding
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.model.Game
-import kotlin.collections.ArrayList
-
-/**
- * This adapter gets its information from a database Cursor. This fact, paired with the usage of
- * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
- * large dataset.
- */
-class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<Game>) :
- RecyclerView.Adapter<GameAdapter.GameViewHolder>(),
+import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
+
+class GameAdapter(private val activity: AppCompatActivity) :
+ ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
View.OnClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
// Create a new view.
- val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context))
+ val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
binding.root.setOnClickListener(this)
// Use that view to create a ViewHolder.
@@ -41,12 +38,10 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
}
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
- holder.bind(games[position])
+ holder.bind(currentList[position])
}
- override fun getItemCount(): Int {
- return games.size
- }
+ override fun getItemCount(): Int = currentList.size
/**
* Launches the game that was clicked on.
@@ -55,7 +50,7 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
*/
override fun onClick(view: View) {
val holder = view.tag as GameViewHolder
- EmulationActivity.launch((view.context as AppCompatActivity), holder.game)
+ EmulationActivity.launch(activity, holder.game)
}
inner class GameViewHolder(val binding: CardGameBinding) :
@@ -74,7 +69,6 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
val bitmap = decodeGameIcon(game.path)
binding.imageGameScreen.load(bitmap) {
error(R.drawable.no_icon)
- crossfade(true)
}
}
@@ -87,9 +81,15 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<
}
}
- fun swapData(games: ArrayList<Game>) {
- this.games = games
- notifyDataSetChanged()
+ private class DiffCallback : DiffUtil.ItemCallback<Game>() {
+ override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
+ return oldItem.gameId == newItem.gameId
+ }
+
+ @SuppressLint("DiffUtilEquals")
+ override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
+ return oldItem == newItem
+ }
}
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 @@
+package org.yuzu.yuzu_emu.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.ResourcesCompat
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
+import org.yuzu.yuzu_emu.model.HomeOption
+
+class HomeOptionAdapter(private val activity: AppCompatActivity, var options: List<HomeOption>) :
+ RecyclerView.Adapter<HomeOptionAdapter.HomeOptionViewHolder>(),
+ View.OnClickListener {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
+ val binding = CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ binding.root.setOnClickListener(this)
+ return HomeOptionViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int {
+ return options.size
+ }
+
+ override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
+ holder.bind(options[position])
+ }
+
+ override fun onClick(view: View) {
+ val holder = view.tag as HomeOptionViewHolder
+ holder.option.onClick.invoke()
+ }
+
+ inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ lateinit var option: HomeOption
+
+ init {
+ itemView.tag = this
+ }
+
+ fun bind(option: HomeOption) {
+ this.option = option
+ binding.optionTitle.text = activity.resources.getString(option.titleId)
+ binding.optionDescription.text = activity.resources.getString(option.descriptionId)
+ binding.optionIcon.setImageDrawable(
+ ResourcesCompat.getDrawable(
+ activity.resources,
+ option.iconId,
+ activity.theme
+ )
+ )
+ }
+ }
+}
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
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
+import com.google.android.material.color.MaterialColors
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@@ -50,6 +51,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
setSupportActionBar(binding.toolbarSettings)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+ ThemeHelper.setNavigationBarColor(
+ this,
+ MaterialColors.getColor(window.decorView, R.attr.colorSurface)
+ )
+
setInsets()
}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeOption
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GameHelper
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import java.io.IOException
+
+class OptionsFragment : Fragment() {
+ private var _binding: FragmentOptionsBinding? = null
+ private val binding get() = _binding!!
+
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentOptionsBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val optionsList: List<HomeOption> = listOf(
+ HomeOption(
+ R.string.add_games,
+ R.string.add_games_description,
+ R.drawable.ic_add
+ ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
+ HomeOption(
+ R.string.install_prod_keys,
+ R.string.install_prod_keys_description,
+ R.drawable.ic_unlock
+ ) { getProdKey.launch(arrayOf("*/*")) },
+ HomeOption(
+ R.string.install_amiibo_keys,
+ R.string.install_amiibo_keys_description,
+ R.drawable.ic_nfc
+ ) { getAmiiboKey.launch(arrayOf("*/*")) },
+ HomeOption(
+ R.string.install_gpu_driver,
+ R.string.install_gpu_driver_description,
+ R.drawable.ic_input
+ ) { driverInstaller() },
+ HomeOption(
+ R.string.settings,
+ R.string.settings_description,
+ R.drawable.ic_settings
+ ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") }
+ )
+
+ binding.optionsList.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ adapter = HomeOptionAdapter(requireActivity() as AppCompatActivity, optionsList)
+ }
+
+ requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity(
+ MaterialColors.getColor(
+ binding.root,
+ R.attr.colorSurface
+ ), ThemeHelper.SYSTEM_BAR_ALPHA
+ )
+
+ setInsets()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ 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)
+ .setPositiveButton(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()
+ }
+ .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
+ getDriver.launch(arrayOf("application/zip"))
+ }
+ .show()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.scrollViewOptions) { view: View, windowInsets: WindowInsetsCompat ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ view.setPadding(
+ insets.left,
+ insets.top,
+ insets.right,
+ insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation)
+ )
+ windowInsets
+ }
+
+ private val getGamesDirectory =
+ registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ requireActivity().contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ // When a new directory is picked, we currently will reset the existing games
+ // database. This effectively means that only one game directory is supported.
+ PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
+ .putString(GameHelper.KEY_GAME_PATH, result.toString())
+ .apply()
+
+ gamesViewModel.reloadGames(true)
+ }
+
+ private val getProdKey =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ requireActivity().contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ val dstPath = DirectoryInitialization.userDirectory + "/keys/"
+ if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) {
+ if (NativeLibrary.reloadKeys()) {
+ Toast.makeText(
+ requireContext(),
+ R.string.install_keys_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ gamesViewModel.reloadGames(true)
+ } else {
+ Toast.makeText(
+ requireContext(),
+ R.string.install_keys_failure,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ private val getAmiiboKey =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ requireActivity().contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ val dstPath = DirectoryInitialization.userDirectory + "/keys/"
+ if (FileUtil.copyUriToInternalStorage(
+ requireContext(),
+ result,
+ dstPath,
+ "key_retail.bin"
+ )
+ ) {
+ if (NativeLibrary.reloadKeys()) {
+ Toast.makeText(
+ requireContext(),
+ R.string.install_keys_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ requireContext(),
+ R.string.install_amiibo_keys_failure,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ private 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
+ requireActivity().contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
+ progressBinding.progressBar.isIndeterminate = true
+ val installationDialog = MaterialAlertDialogBuilder(requireContext())
+ .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(requireContext(), result)
+ } catch (_: IOException) {
+ }
+
+ withContext(Dispatchers.Main) {
+ installationDialog.dismiss()
+
+ val driverName = GpuDriverHelper.customDriverName
+ if (driverName != null) {
+ Toast.makeText(
+ requireContext(),
+ getString(
+ R.string.select_gpu_driver_install_success,
+ driverName
+ ),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ requireContext(),
+ R.string.select_gpu_driver_error,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+ }
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
package org.yuzu.yuzu_emu.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.utils.GameHelper
class GamesViewModel : ViewModel() {
- private val _games = MutableLiveData<ArrayList<Game>>()
- val games: LiveData<ArrayList<Game>> get() = _games
+ private val _games = MutableLiveData<List<Game>>(emptyList())
+ val games: LiveData<List<Game>> get() = _games
+
+ private val _searchedGames = MutableLiveData<List<Game>>(emptyList())
+ val searchedGames: LiveData<List<Game>> get() = _searchedGames
+
+ private val _isReloading = MutableLiveData(false)
+ val isReloading: LiveData<Boolean> get() = _isReloading
+
+ private val _shouldSwapData = MutableLiveData(false)
+ val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData
init {
- _games.value = ArrayList()
+ reloadGames(false)
+ }
+
+ fun setSearchedGames(games: List<Game>) {
+ _searchedGames.postValue(games)
+ }
+
+ fun setShouldSwapData(shouldSwap: Boolean) {
+ _shouldSwapData.postValue(shouldSwap)
}
- fun setGames(games: ArrayList<Game>) {
- _games.value = games
+ fun reloadGames(directoryChanged: Boolean) {
+ if (isReloading.value == true)
+ return
+ _isReloading.postValue(true)
+
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ NativeLibrary.resetRomMetadata()
+ _games.postValue(GameHelper.getGames())
+ _isReloading.postValue(false)
+
+ if (directoryChanged) {
+ setShouldSwapData(true)
+ }
+ }
+ }
}
}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+data class HomeOption(
+ val titleId: Int,
+ val descriptionId: Int,
+ val iconId: Int,
+ val onClick: () -> Unit
+)
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 @@
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class HomeViewModel : ViewModel() {
+ private val _navigationVisible = MutableLiveData(true)
+ val navigationVisible: LiveData<Boolean> get() = _navigationVisible
+
+ fun setNavigationVisible(visible: Boolean) {
+ if (_navigationVisible.value == visible) {
+ return
+ }
+ _navigationVisible.value = visible
+ }
+}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.color.MaterialColors
+import com.google.android.material.search.SearchView
+import com.google.android.material.search.SearchView.TransitionState
+import info.debatty.java.stringsimilarity.Jaccard
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.GameAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
+import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.ThemeHelper
+import java.util.Locale
+
+class GamesFragment : Fragment() {
+ private var _binding: FragmentGamesBinding? = null
+ private val binding get() = _binding!!
+
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentGamesBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ // Use custom back navigation so the user doesn't back out of the app when trying to back
+ // out of the search view
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (binding.searchView.currentTransitionState == TransitionState.SHOWN) {
+ binding.searchView.hide()
+ } else {
+ requireActivity().finish()
+ }
+ }
+ })
+
+ binding.gridGames.apply {
+ layoutManager = AutofitGridLayoutManager(
+ requireContext(),
+ requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
+ )
+ adapter = GameAdapter(requireActivity() as AppCompatActivity)
+ }
+ setUpSearch()
+
+ // Add swipe down to refresh gesture
+ binding.swipeRefresh.setOnRefreshListener {
+ gamesViewModel.reloadGames(false)
+ }
+
+ // Set theme color to the refresh animation's background
+ binding.swipeRefresh.setProgressBackgroundColorSchemeColor(
+ MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary)
+ )
+ binding.swipeRefresh.setColorSchemeColors(
+ MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary)
+ )
+
+ // Watch for when we get updates to any of our games lists
+ gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading ->
+ binding.swipeRefresh.isRefreshing = isReloading
+
+ if (!isReloading) {
+ if (gamesViewModel.games.value!!.isEmpty()) {
+ binding.noticeText.visibility = View.VISIBLE
+ } else {
+ binding.noticeText.visibility = View.GONE
+ }
+ }
+ }
+ gamesViewModel.games.observe(viewLifecycleOwner) {
+ (binding.gridGames.adapter as GameAdapter).submitList(it)
+ }
+ gamesViewModel.searchedGames.observe(viewLifecycleOwner) {
+ (binding.gridSearch.adapter as GameAdapter).submitList(it)
+ }
+ gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
+ if (shouldSwapData) {
+ (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
+ gamesViewModel.setShouldSwapData(false)
+ }
+ }
+
+ // Hide bottom navigation and FAB when using the search view
+ binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState ->
+ when (newState) {
+ TransitionState.SHOWING,
+ TransitionState.SHOWN -> {
+ (binding.gridSearch.adapter as GameAdapter).submitList(emptyList())
+ searchShown()
+ }
+ TransitionState.HIDDEN,
+ TransitionState.HIDING -> {
+ gamesViewModel.setSearchedGames(emptyList())
+ searchHidden()
+ }
+ }
+ }
+
+ // Ensure that bottom navigation or FAB don't appear upon recreation
+ val searchState = binding.searchView.currentTransitionState
+ if (searchState == TransitionState.SHOWN) {
+ searchShown()
+ } else if (searchState == TransitionState.HIDDEN) {
+ searchHidden()
+ }
+
+ setInsets()
+
+ // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn
+ binding.swipeRefresh.post {
+ binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!!
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ private fun searchShown() {
+ homeViewModel.setNavigationVisible(false)
+ requireActivity().window.statusBarColor =
+ ContextCompat.getColor(requireContext(), android.R.color.transparent)
+ }
+
+ private fun searchHidden() {
+ homeViewModel.setNavigationVisible(true)
+ requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity(
+ MaterialColors.getColor(
+ binding.root,
+ R.attr.colorSurface
+ ), ThemeHelper.SYSTEM_BAR_ALPHA
+ )
+ }
+
+ private inner class ScoredGame(val score: Double, val item: Game)
+
+ private fun setUpSearch() {
+ binding.gridSearch.apply {
+ layoutManager = AutofitGridLayoutManager(
+ requireContext(),
+ requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
+ )
+ adapter = GameAdapter(requireActivity() as AppCompatActivity)
+ }
+
+ binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
+ val searchTerm = text.toString().lowercase(Locale.getDefault())
+ val searchAlgorithm = Jaccard(2)
+ val sortedList: List<Game> = gamesViewModel.games.value!!.mapNotNull { game ->
+ val title = game.title.lowercase(Locale.getDefault())
+ val score = searchAlgorithm.similarity(searchTerm, title)
+ if (score > 0.03) {
+ ScoredGame(score, game)
+ } else {
+ null
+ }
+ }.sortedByDescending { it.score }.map { it.item }
+ gamesViewModel.setSearchedGames(sortedList)
+ }
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
+
+ view.setPadding(
+ insets.left,
+ insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search),
+ insets.right,
+ insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
+ )
+ binding.gridSearch.updatePadding(
+ left = insets.left,
+ top = extraListSpacing,
+ right = insets.right,
+ bottom = insets.bottom + extraListSpacing
+ )
+
+ binding.swipeRefresh.setSlingshotDistance(
+ resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot)
+ )
+ binding.swipeRefresh.setProgressViewOffset(
+ false,
+ insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start),
+ insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
+ )
+
+ windowInsets
+ }
+}
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 @@
package org.yuzu.yuzu_emu.ui.main
-import android.content.DialogInterface
-import android.content.Intent
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
import android.view.View
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
+import android.view.ViewGroup.MarginLayoutParams
+import android.view.animation.PathInterpolator
+import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updatePadding
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.yuzu.yuzu_emu.NativeLibrary
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.setupWithNavController
+import com.google.android.material.color.MaterialColors
+import com.google.android.material.elevation.ElevationOverlayProvider
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.settings.ui.SettingsActivity
-import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment
+import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.*
-import java.io.IOException
-
-class MainActivity : AppCompatActivity(), MainView {
- private var platformGamesFragment: PlatformGamesFragment? = null
- private val presenter = MainPresenter(this)
+class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
+ private val homeViewModel: HomeViewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@@ -52,19 +41,36 @@ class MainActivity : AppCompatActivity(), MainView {
WindowCompat.setDecorFitsSystemWindows(window, false)
- setSupportActionBar(binding.toolbarMain)
- presenter.onCreate()
- if (savedInstanceState == null) {
- StartupHandler.handleInit(this)
- platformGamesFragment = PlatformGamesFragment()
- supportFragmentManager.beginTransaction()
- .add(R.id.games_platform_frame, platformGamesFragment!!)
- .commit()
- } else {
- platformGamesFragment = supportFragmentManager.getFragment(
- savedInstanceState,
- PlatformGamesFragment.TAG
- ) as PlatformGamesFragment?
+ ThemeHelper.setNavigationBarColor(
+ this,
+ ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay(
+ MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface),
+ binding.navigationBar.elevation
+ )
+ )
+
+ // Set up a central host fragment that is controlled via bottom navigation with xml navigation
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+ binding.navigationBar.setupWithNavController(navHostFragment.navController)
+
+ binding.statusBarShade.setBackgroundColor(
+ ThemeHelper.getColorWithOpacity(
+ MaterialColors.getColor(
+ binding.root,
+ R.attr.colorSurface
+ ), ThemeHelper.SYSTEM_BAR_ALPHA
+ )
+ )
+
+ // Prevents navigation from being drawn for a short time on recreation if set to hidden
+ if (homeViewModel.navigationVisible.value == false) {
+ binding.navigationBar.visibility = View.INVISIBLE
+ binding.statusBarShade.visibility = View.INVISIBLE
+ }
+
+ homeViewModel.navigationVisible.observe(this) { visible ->
+ showNavigation(visible)
}
// Dismiss previous notifications (should not happen unless a crash occurred)
@@ -73,78 +79,24 @@ class MainActivity : AppCompatActivity(), MainView {
setInsets()
}
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- supportFragmentManager.putFragment(
- outState,
- PlatformGamesFragment.TAG,
- platformGamesFragment!!
- )
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_game_grid, menu)
- return true
- }
-
- /**
- * MainView
- */
- override fun setVersionString(version: String) {
- binding.toolbarMain.subtitle = version
- }
-
- override fun launchSettingsActivity(menuTag: String) {
- SettingsActivity.launch(this, menuTag, "")
- }
-
- override fun launchFileListActivity(request: Int) {
- when (request) {
- MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
- MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*"))
- MainPresenter.REQUEST_INSTALL_AMIIBO_KEYS -> getAmiiboKey.launch(arrayOf("*/*"))
- MainPresenter.REQUEST_SELECT_GPU_DRIVER -> {
- // Get the driver name for the dialog message.
- var driverName = GpuDriverHelper.customDriverName
- if (driverName == null) {
- driverName = getString(R.string.system_gpu_driver)
- }
-
- MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.select_gpu_driver_title))
- .setMessage(driverName)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
- GpuDriverHelper.installDefaultDriver(this)
- Toast.makeText(
- this,
- R.string.select_gpu_driver_use_default,
- Toast.LENGTH_SHORT
- ).show()
- }
- .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
- getDriver.launch(arrayOf("application/zip"))
- }
- .show()
+ private fun showNavigation(visible: Boolean) {
+ binding.navigationBar.animate().apply {
+ if (visible) {
+ binding.navigationBar.visibility = View.VISIBLE
+ binding.navigationBar.translationY = binding.navigationBar.height.toFloat() * 2
+ duration = 300
+ translationY(0f)
+ interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
+ } else {
+ duration = 300
+ translationY(binding.navigationBar.height.toFloat() * 2)
+ interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
}
- }
- }
-
- /**
- * Called by the framework whenever any actionbar/toolbar icon is clicked.
- *
- * @param item The icon that was clicked on.
- * @return True if the event was handled, false to bubble it up to the OS.
- */
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return presenter.handleOptionSelection(item.itemId)
- }
-
- private fun refreshFragment() {
- if (platformGamesFragment != null) {
- NativeLibrary.resetRomMetadata()
- platformGamesFragment!!.refresh()
- }
+ }.withEndAction {
+ if (!visible) {
+ binding.navigationBar.visibility = View.INVISIBLE
+ }
+ }.start()
}
override fun onDestroy() {
@@ -152,145 +104,12 @@ class MainActivity : AppCompatActivity(), MainView {
super.onDestroy()
}
- private fun setInsets() {
- ViewCompat.setOnApplyWindowInsetsListener(binding.gamesPlatformFrame) { view: View, windowInsets: WindowInsetsCompat ->
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.statusBarShade) { view: View, windowInsets: WindowInsetsCompat ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- view.updatePadding(left = insets.left, right = insets.right)
- InsetsHelper.insetAppBar(insets, binding.appbarMain)
+ val mlpShade = view.layoutParams as MarginLayoutParams
+ mlpShade.height = insets.top
+ binding.statusBarShade.layoutParams = mlpShade
windowInsets
}
- }
-
- private val getGamesDirectory =
- registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { 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
- )
-
- // When a new directory is picked, we currently will reset the existing games
- // database. This effectively means that only one game directory is supported.
- PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
- .putString(GameHelper.KEY_GAME_PATH, result.toString())
- .apply()
- }
-
- private val getProdKey =
- 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 dstPath = DirectoryInitialization.userDirectory + "/keys/"
- if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "prod.keys")) {
- if (NativeLibrary.reloadKeys()) {
- Toast.makeText(
- this,
- R.string.install_keys_success,
- Toast.LENGTH_SHORT
- ).show()
- refreshFragment()
- } else {
- Toast.makeText(
- this,
- R.string.install_keys_failure,
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
-
- private val getAmiiboKey =
- 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 dstPath = DirectoryInitialization.userDirectory + "/keys/"
- if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "key_retail.bin")) {
- if (NativeLibrary.reloadKeys()) {
- Toast.makeText(
- this,
- R.string.install_keys_success,
- Toast.LENGTH_SHORT
- ).show()
- refreshFragment()
- } else {
- Toast.makeText(
- this,
- R.string.install_amiibo_keys_failure,
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
-
- private 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()
- }
- }
- }
- }
- }
}
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 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.ui.main
-
-import org.yuzu.yuzu_emu.BuildConfig
-import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
-
-class MainPresenter(private val view: MainView) {
- fun onCreate() {
- val versionName = BuildConfig.VERSION_NAME
- view.setVersionString(versionName)
- }
-
- private fun launchFileListActivity(request: Int) {
- view.launchFileListActivity(request)
- }
-
- fun handleOptionSelection(itemId: Int): Boolean {
- when (itemId) {
- R.id.menu_settings_core -> {
- view.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG)
- return true
- }
- R.id.button_add_directory -> {
- launchFileListActivity(REQUEST_ADD_DIRECTORY)
- return true
- }
- R.id.button_install_keys -> {
- launchFileListActivity(REQUEST_INSTALL_KEYS)
- return true
- }
- R.id.button_install_amiibo_keys -> {
- launchFileListActivity(REQUEST_INSTALL_AMIIBO_KEYS)
- return true
- }
- R.id.button_select_gpu_driver -> {
- launchFileListActivity(REQUEST_SELECT_GPU_DRIVER)
- return true
- }
- }
- return false
- }
-
- companion object {
- const val REQUEST_ADD_DIRECTORY = 1
- const val REQUEST_INSTALL_KEYS = 2
- const val REQUEST_INSTALL_AMIIBO_KEYS = 3
- const val REQUEST_SELECT_GPU_DRIVER = 4
- }
-}
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 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.ui.main
-
-/**
- * Abstraction for the screen that shows on application launch.
- * Implementations will differ primarily to target touch-screen
- * or non-touch screen devices.
- */
-interface MainView {
- /**
- * Pass the view the native library's version string. Displaying
- * it is optional.
- *
- * @param version A string pulled from native code.
- */
- fun setVersionString(version: String)
-
- fun launchSettingsActivity(menuTag: String)
-
- fun launchFileListActivity(request: Int)
-}
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 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.ui.platform
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updatePadding
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
-import com.google.android.material.color.MaterialColors
-import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.adapters.GameAdapter
-import org.yuzu.yuzu_emu.databinding.FragmentGridBinding
-import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
-import org.yuzu.yuzu_emu.model.GamesViewModel
-import org.yuzu.yuzu_emu.utils.GameHelper
-
-class PlatformGamesFragment : Fragment() {
- private var _binding: FragmentGridBinding? = null
- private val binding get() = _binding!!
-
- private lateinit var gamesViewModel: GamesViewModel
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- _binding = FragmentGridBinding.inflate(inflater)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java]
-
- binding.gridGames.apply {
- layoutManager = AutofitGridLayoutManager(
- requireContext(),
- requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
- )
- adapter =
- GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!)
- }
-
- // Add swipe down to refresh gesture
- binding.swipeRefresh.setOnRefreshListener {
- refresh()
- binding.swipeRefresh.isRefreshing = false
- }
-
- // Set theme color to the refresh animation's background
- binding.swipeRefresh.setProgressBackgroundColorSchemeColor(
- MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary)
- )
- binding.swipeRefresh.setColorSchemeColors(
- MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary)
- )
-
- gamesViewModel.games.observe(viewLifecycleOwner) {
- (binding.gridGames.adapter as GameAdapter).swapData(it)
- updateTextView()
- }
-
- setInsets()
-
- refresh()
- }
-
- override fun onResume() {
- super.onResume()
- refresh()
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-
- fun refresh() {
- gamesViewModel.setGames(GameHelper.getGames())
- updateTextView()
- }
-
- private fun updateTextView() {
- if (_binding == null)
- return
-
- binding.gamelistEmptyText.visibility =
- if ((binding.gridGames.adapter as GameAdapter).itemCount == 0) View.VISIBLE else View.GONE
- }
-
- private fun setInsets() {
- ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat ->
- val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- view.updatePadding(bottom = insets.bottom)
- windowInsets
- }
- }
-
- companion object {
- const val TAG = "PlatformGamesFragment"
- }
-}
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 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.utils
-
-import androidx.preference.PreferenceManager
-import android.text.Html
-import android.text.method.LinkMovementMethod
-import android.view.View
-import android.widget.TextView
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.features.settings.model.Settings
-import org.yuzu.yuzu_emu.ui.main.MainActivity
-import org.yuzu.yuzu_emu.ui.main.MainPresenter
-
-object StartupHandler {
- private val preferences =
- PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
-
- private fun handleStartupPromptDismiss(parent: MainActivity) {
- parent.launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS)
- }
-
- private fun markFirstBoot() {
- preferences.edit()
- .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
- .apply()
- }
-
- fun handleInit(parent: MainActivity) {
- if (preferences.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)) {
- markFirstBoot()
- val alert = MaterialAlertDialogBuilder(parent)
- .setMessage(Html.fromHtml(parent.resources.getString(R.string.app_disclaimer)))
- .setTitle(R.string.app_name)
- .setIcon(R.drawable.ic_launcher)
- .setPositiveButton(android.R.string.ok, null)
- .setOnDismissListener {
- handleStartupPromptDismiss(parent)
- }
- .show()
- (alert.findViewById<View>(android.R.id.message) as TextView?)!!.movementMethod =
- LinkMovementMethod.getInstance()
- }
- }
-}
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
import kotlin.math.roundToInt
object ThemeHelper {
- private const val NAV_BAR_ALPHA = 0.9f
+ const val SYSTEM_BAR_ALPHA = 0.9f
@JvmStatic
fun setTheme(activity: AppCompatActivity) {
@@ -29,10 +29,6 @@ object ThemeHelper {
windowController.isAppearanceLightNavigationBars = isLightMode
activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.transparent)
-
- val navigationBarColor =
- MaterialColors.getColor(activity.window.decorView, R.attr.colorSurface)
- setNavigationBarColor(activity, navigationBarColor)
}
@JvmStatic
@@ -48,7 +44,7 @@ object ThemeHelper {
} else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION
) {
- activity.window.navigationBarColor = getColorWithOpacity(color, NAV_BAR_ALPHA)
+ activity.window.navigationBarColor = getColorWithOpacity(color, SYSTEM_BAR_ALPHA)
} else {
activity.window.navigationBarColor = ContextCompat.getColor(
activity.applicationContext,
@@ -58,7 +54,7 @@ object ThemeHelper {
}
@ColorInt
- private fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
+ fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
return Color.argb(
(alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color),
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 @@
+<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,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ 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" />
+</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 @@
+<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="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" />
+</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 @@
+<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_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 @@
+<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="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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="614.697dp"
+ android:height="683dp"
+ android:viewportWidth="614.4"
+ android:viewportHeight="682.67">
+ <group>
+ <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" />
+ <path
+ android:fillColor="?attr/colorPrimary"
+ 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" />
+ </group>
+ <group>
+ <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" />
+ <path
+ android:fillColor="?attr/colorPrimary"
+ 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" />
+ </group>
+</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 @@
<?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+ 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"
android:id="@+id/coordinator_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <com.google.android.material.appbar.AppBarLayout
- android:id="@+id/appbar_main"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:fitsSystemWindows="true"
- app:liftOnScrollTargetViewId="@id/grid_games">
-
- <androidx.appcompat.widget.Toolbar
- android:id="@+id/toolbar_main"
- android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize" />
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment_container"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/home_navigation"
+ tools:layout="@layout/fragment_games" />
- </com.google.android.material.appbar.AppBarLayout>
-
- <FrameLayout
- android:id="@+id/games_platform_frame"
+ <com.google.android.material.bottomnavigation.BottomNavigationView
+ android:id="@+id/navigation_bar"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:menu="@menu/menu_navigation" />
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
+</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 @@
+<?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/materialCardViewFilledStyle"
+ android:id="@+id/option_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="8dp"
+ android:layout_marginHorizontal="16dp"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/option_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginStart="28dp"
+ android:layout_gravity="center_vertical"
+ app:tint="?attr/colorPrimary" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:id="@+id/option_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ tools:text="@string/install_prod_keys" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodySmall"
+ android:id="@+id/option_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ tools:text="@string/install_prod_keys_description" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</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 @@
+<?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"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/coordinator_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+ android:id="@+id/swipe_refresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ app:layout_behavior="@string/searchbar_scrolling_view_behavior">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/notice_text"
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:padding="@dimen/spacing_large"
+ android:text="@string/empty_gamelist"
+ tools:visibility="gone" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/grid_games"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ tools:listitem="@layout/card_game" />
+
+ </RelativeLayout>
+
+ </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/app_bar_search"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:liftOnScrollTargetViewId="@id/grid_games">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true">
+
+ <com.google.android.material.search.SearchBar
+ android:id="@+id/search_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/home_search_games" />
+
+ </FrameLayout>
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <com.google.android.material.search.SearchView
+ android:id="@+id/search_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:hint="@string/home_search_games"
+ app:layout_anchor="@id/search_bar">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/grid_search"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ tools:listitem="@layout/card_game" />
+
+ </com.google.android.material.search.SearchView>
+
+</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 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
- android:id="@+id/swipe_refresh"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:id="@+id/gamelist_empty_text"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center"
- android:text="@string/empty_gamelist"
- android:textSize="18sp"
- android:visibility="gone" />
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/grid_games"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipToPadding="false"
- tools:listitem="@layout/card_game" />
-
- </RelativeLayout>
-
- </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
-
-</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.core.widget.NestedScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/scroll_view_options"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface"
+ android:clipToPadding="false">
+
+ <androidx.appcompat.widget.LinearLayoutCompat
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="?attr/colorSurface">
+
+ <ImageView
+ android:layout_width="128dp"
+ android:layout_height="128dp"
+ android:layout_margin="64dp"
+ android:layout_gravity="center_horizontal"
+ android:src="@drawable/ic_yuzu_themed" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/options_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </androidx.appcompat.widget.LinearLayoutCompat>
+
+</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 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <item
- android:id="@+id/button_file_menu"
- android:icon="@drawable/ic_folder"
- android:title="@string/select_game_folder"
- app:showAsAction="ifRoom">
-
- <menu>
-
- <item
- android:id="@+id/button_add_directory"
- android:icon="@drawable/ic_folder"
- android:title="@string/select_game_folder"
- app:showAsAction="ifRoom" />
-
- <item
- android:id="@+id/button_install_keys"
- android:icon="@drawable/ic_install"
- android:title="@string/install_keys"
- app:showAsAction="ifRoom" />
-
- <item
- android:id="@+id/button_install_amiibo_keys"
- android:icon="@drawable/ic_install"
- android:title="@string/install_amiibo_keys"
- app:showAsAction="ifRoom" />
-
- <item
- android:id="@+id/button_select_gpu_driver"
- android:icon="@drawable/ic_settings"
- android:title="@string/select_gpu_driver"
- app:showAsAction="ifRoom" />
-
- </menu>
-
- </item>
-
- <item
- android:id="@+id/menu_settings_core"
- android:icon="@drawable/ic_settings"
- android:title="@string/grid_menu_core_settings"
- app:showAsAction="ifRoom" />
-
-</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/gamesFragment"
+ android:icon="@drawable/ic_controller"
+ android:title="@string/home_games" />
+
+ <item
+ android:id="@+id/optionsFragment"
+ android:icon="@drawable/ic_options"
+ android:title="@string/home_options" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/home_navigation"
+ app:startDestination="@id/gamesFragment">
+
+ <fragment
+ android:id="@+id/gamesFragment"
+ android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
+ android:label="PlatformGamesFragment" />
+
+ <fragment
+ android:id="@+id/optionsFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment"
+ android:label="OptionsFragment" />
+
+</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 @@
<resources>
<dimen name="spacing_small">4dp</dimen>
+ <dimen name="spacing_med">8dp</dimen>
<dimen name="spacing_medlarge">12dp</dimen>
<dimen name="spacing_large">16dp</dimen>
<dimen name="spacing_xtralarge">32dp</dimen>
<dimen name="spacing_list">64dp</dimen>
- <dimen name="spacing_fab">72dp</dimen>
+ <dimen name="spacing_navigation">80dp</dimen>
+ <dimen name="spacing_search">88dp</dimen>
+ <dimen name="spacing_refresh_slingshot">80dp</dimen>
+ <dimen name="spacing_refresh_start">32dp</dimen>
+ <dimen name="spacing_refresh_end">96dp</dimen>
<dimen name="menu_width">256dp</dimen>
<dimen name="card_width">160dp</dimen>
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 @@
<string name="app_notification_channel_description">yuzu Switch emulator notifications</string>
<string name="app_notification_running">yuzu is running</string>
+ <!-- Home strings -->
+ <string name="home_games">Games</string>
+ <string name="home_options">Options</string>
+ <string name="add_games">Add Games</string>
+ <string name="add_games_description">Select your games folder</string>
+ <string name="home_search_games">Search Games</string>
+ <string name="install_prod_keys">Install Prod.keys</string>
+ <string name="install_prod_keys_description">Required to decrypt retail games</string>
+ <string name="install_amiibo_keys">Install Amiibo Keys</string>
+ <string name="install_amiibo_keys_description">Required to use Amiibo in game</string>
+ <string name="install_keys_success">Keys successfully installed</string>
+ <string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
+ <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string>
+ <string name="install_gpu_driver">Install GPU Driver</string>
+ <string name="install_gpu_driver_description">Use a different driver for potentially better performance or accuracy</string>
+ <string name="settings">Settings</string>
+ <string name="settings_description">Configure emulator settings</string>
+
<!-- General settings strings -->
<string name="frame_limit_enable">Enable limit speed</string>
<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 @@
<string name="error_saving">Error saving %1$s.ini: %2$s</string>
<string name="loading">Loading...</string>
- <!-- Game Grid Screen-->
- <string name="grid_menu_core_settings">Settings</string>
-
- <!-- Add Directory Screen-->
- <string name="select_game_folder">Select game folder</string>
- <string name="install_keys">Install keys</string>
- <string name="install_amiibo_keys">Install amiibo keys</string>
- <string name="install_keys_success">Keys successfully installed</string>
- <string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
- <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</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>