From 59525ddbebc4c1a8add986eabd7802a62fedc71b Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Wed, 19 Apr 2023 22:42:18 -0400 Subject: android: First time setup screen --- .../org/yuzu/yuzu_emu/adapters/SetupAdapter.kt | 70 +++++++ .../org/yuzu/yuzu_emu/fragments/OptionsFragment.kt | 167 +---------------- .../org/yuzu/yuzu_emu/fragments/SetupFragment.kt | 206 +++++++++++++++++++++ .../java/org/yuzu/yuzu_emu/model/HomeViewModel.kt | 2 + .../main/java/org/yuzu/yuzu_emu/model/SetupPage.kt | 14 ++ .../java/org/yuzu/yuzu_emu/ui/GamesFragment.kt | 6 + .../java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 191 ++++++++++++++++++- .../app/src/main/res/drawable/ic_arrow_forward.xml | 10 + src/android/app/src/main/res/drawable/ic_check.xml | 9 + .../app/src/main/res/drawable/ic_controller.xml | 2 +- src/android/app/src/main/res/drawable/ic_key.xml | 9 + .../app/src/main/res/drawable/ic_yuzu_title.xml | 24 +++ .../src/main/res/layout-w600dp/fragment_setup.xml | 38 ++++ .../app/src/main/res/layout-w600dp/page_setup.xml | 65 +++++++ .../app/src/main/res/layout/activity_main.xml | 4 +- .../app/src/main/res/layout/fragment_setup.xml | 38 ++++ src/android/app/src/main/res/layout/page_setup.xml | 52 ++++++ .../src/main/res/navigation/home_navigation.xml | 9 + src/android/app/src/main/res/values/strings.xml | 16 ++ 19 files changed, 769 insertions(+), 163 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt create mode 100644 src/android/app/src/main/res/drawable/ic_arrow_forward.xml create mode 100644 src/android/app/src/main/res/drawable/ic_check.xml create mode 100644 src/android/app/src/main/res/drawable/ic_key.xml create mode 100644 src/android/app/src/main/res/drawable/ic_yuzu_title.xml create mode 100644 src/android/app/src/main/res/layout-w600dp/fragment_setup.xml create mode 100644 src/android/app/src/main/res/layout-w600dp/page_setup.xml create mode 100644 src/android/app/src/main/res/layout/fragment_setup.xml create mode 100644 src/android/app/src/main/res/layout/page_setup.xml diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt new file mode 100644 index 000000000..481ddd5a5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.text.Html +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import org.yuzu.yuzu_emu.databinding.PageSetupBinding +import org.yuzu.yuzu_emu.model.SetupPage + +class SetupAdapter(val activity: AppCompatActivity, val pages: List) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder { + val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SetupPageViewHolder(binding) + } + + override fun getItemCount(): Int = pages.size + + override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) = + holder.bind(pages[position]) + + inner class SetupPageViewHolder(val binding: PageSetupBinding) : + RecyclerView.ViewHolder(binding.root) { + lateinit var page: SetupPage + + init { + itemView.tag = this + } + + fun bind(page: SetupPage) { + this.page = page + binding.icon.setImageDrawable( + ResourcesCompat.getDrawable( + activity.resources, + page.iconId, + activity.theme + ) + ) + binding.textTitle.text = activity.resources.getString(page.titleId) + binding.textDescription.text = + Html.fromHtml(activity.resources.getString(page.descriptionId), 0) + + binding.buttonAction.apply { + text = activity.resources.getString(page.buttonTextId) + if (page.buttonIconId != 0) { + icon = ResourcesCompat.getDrawable( + activity.resources, + page.buttonIconId, + activity.theme + ) + } + iconGravity = + if (page.leftAlignedIcon) { + MaterialButton.ICON_GRAVITY_START + } else { + MaterialButton.ICON_GRAVITY_END + } + setOnClickListener { + page.buttonAction.invoke() + } + } + } + } +} 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 index 954e52dc6..1cf0d0f52 100644 --- 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 @@ -10,39 +10,26 @@ 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.ui.main.MainActivity 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() + private lateinit var mainActivity: MainActivity override fun onCreateView( inflater: LayoutInflater, @@ -54,22 +41,24 @@ class OptionsFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + mainActivity = requireActivity() as MainActivity + val optionsList: List = listOf( HomeOption( R.string.add_games, R.string.add_games_description, R.drawable.ic_add - ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, + ) { mainActivity.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("*/*")) }, + ) { mainActivity.getProdKey.launch(arrayOf("*/*")) }, HomeOption( R.string.install_amiibo_keys, R.string.install_amiibo_keys_description, R.drawable.ic_nfc - ) { getAmiiboKey.launch(arrayOf("*/*")) }, + ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }, HomeOption( R.string.install_gpu_driver, R.string.install_gpu_driver_description, @@ -115,7 +104,7 @@ class OptionsFragment : Fragment() { ).show() } .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> - getDriver.launch(arrayOf("application/zip")) + mainActivity.getDriver.launch(arrayOf("application/zip")) } .show() } @@ -131,144 +120,4 @@ class OptionsFragment : Fragment() { ) 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/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt new file mode 100644 index 000000000..e7d102aad --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.Intent +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.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController +import androidx.preference.PreferenceManager +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.google.android.material.transition.MaterialFadeThrough +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.adapters.SetupAdapter +import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding +import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.SetupPage +import org.yuzu.yuzu_emu.ui.main.MainActivity + +class SetupFragment : Fragment() { + private var _binding: FragmentSetupBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + + private lateinit var mainActivity: MainActivity + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + exitTransition = MaterialFadeThrough() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSetupBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + mainActivity = requireActivity() as MainActivity + + homeViewModel.setNavigationVisibility(false) + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (binding.viewPager2.currentItem > 0) { + pageBackward() + } else { + requireActivity().finish() + } + } + }) + + requireActivity().window.navigationBarColor = + ContextCompat.getColor(requireContext(), android.R.color.transparent) + + val pages = listOf( + SetupPage( + R.drawable.ic_yuzu_title, + R.string.welcome, + R.string.welcome_description, + 0, + true, + R.string.get_started + ) { pageForward() }, + SetupPage( + R.drawable.ic_key, + R.string.keys, + R.string.keys_description, + R.drawable.ic_add, + true, + R.string.select_keys + ) { mainActivity.getProdKey.launch(arrayOf("*/*")) }, + SetupPage( + R.drawable.ic_controller, + R.string.games, + R.string.games_description, + R.drawable.ic_add, + true, + R.string.add_games + ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, + SetupPage( + R.drawable.ic_check, + R.string.done, + R.string.done_description, + R.drawable.ic_arrow_forward, + false, + R.string.text_continue + ) { finishSetup() } + ) + binding.viewPager2.apply { + adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) + offscreenPageLimit = 2 + } + + binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageScrolled( + position: Int, + positionOffset: Float, + positionOffsetPixels: Int + ) { + super.onPageScrolled(position, positionOffset, positionOffsetPixels) + if (position == 0) { + hideView(binding.buttonBack) + } else { + showView(binding.buttonBack) + } + + if (position == pages.size - 1 || position == 0) { + hideView(binding.buttonNext) + } else { + showView(binding.buttonNext) + } + } + }) + + binding.buttonNext.setOnClickListener { pageForward() } + binding.buttonBack.setOnClickListener { pageBackward() } + + if (binding.viewPager2.currentItem == 0) { + binding.buttonNext.visibility = View.INVISIBLE + binding.buttonBack.visibility = View.INVISIBLE + } + + setInsets() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun finishSetup() { + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() + .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) + .apply() + mainActivity.finishSetup(binding.root.findNavController()) + } + + private fun showView(view: View) { + if (view.visibility == View.VISIBLE) { + return + } + + view.apply { + alpha = 0f + visibility = View.VISIBLE + isClickable = true + }.animate().apply { + duration = 300 + alpha(1f) + }.start() + } + + private fun hideView(view: View) { + if (view.visibility == View.GONE) { + return + } + + view.apply { + alpha = 1f + isClickable = false + }.animate().apply { + duration = 300 + alpha(0f) + }.withEndAction { + view.visibility = View.INVISIBLE + } + } + + private fun pageForward() { + binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1 + } + + private fun pageBackward() { + binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1 + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.setupRoot) { view: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + insets.left, + insets.top, + insets.right, + insets.bottom + ) + windowInsets + } +} 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 index b3f4188cd..acda8663a 100644 --- 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 @@ -11,6 +11,8 @@ class HomeViewModel : ViewModel() { private val _statusBarShadeVisible = MutableLiveData(true) val statusBarShadeVisible: LiveData get() = _statusBarShadeVisible + var navigatedToSetup = false + fun setNavigationVisibility(visible: Boolean) { if (_navigationVisible.value == visible) { return diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt new file mode 100644 index 000000000..a8a934552 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +data class SetupPage( + val iconId: Int, + val titleId: Int, + val descriptionId: Int, + val buttonIconId: Int, + val leftAlignedIcon: Boolean, + val buttonTextId: Int, + val buttonAction: () -> Unit +) 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 index c6bbc3c65..759ff18fc 100644 --- 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 @@ -18,6 +18,7 @@ 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 com.google.android.material.transition.MaterialFadeThrough import info.debatty.java.stringsimilarity.Jaccard import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.GameAdapter @@ -35,6 +36,11 @@ class GamesFragment : Fragment() { private val gamesViewModel: GamesViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialFadeThrough() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, 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 e47866030..b455b7d35 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,10 +3,13 @@ package org.yuzu.yuzu_emu.ui.main +import android.content.Intent import android.os.Bundle import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.view.animation.PathInterpolator +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -14,20 +17,33 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController +import androidx.preference.PreferenceManager import com.google.android.material.color.MaterialColors +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.elevation.ElevationOverlayProvider +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.activities.EmulationActivity import org.yuzu.yuzu_emu.databinding.ActivityMainBinding +import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding +import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.* +import java.io.IOException class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val homeViewModel: HomeViewModel by viewModels() + private val gamesViewModel: GamesViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() @@ -52,10 +68,9 @@ class MainActivity : AppCompatActivity() { ) ) - // 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) + setUpNavigation(navHostFragment.navController) binding.statusBarShade.setBackgroundColor( ThemeHelper.getColorWithOpacity( @@ -85,6 +100,32 @@ class MainActivity : AppCompatActivity() { setInsets() } + fun finishSetup(navController: NavController) { + navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment) + binding.navigationBar.setupWithNavController(navController) + showNavigation(true) + + ThemeHelper.setNavigationBarColor( + this, + ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay( + MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface), + binding.navigationBar.elevation + ) + ) + } + + private fun setUpNavigation(navController: NavController) { + val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext) + .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) + + if (firstTimeSetup && !homeViewModel.navigatedToSetup) { + navController.navigate(R.id.firstTimeSetupFragment) + homeViewModel.navigatedToSetup = true + } else { + binding.navigationBar.setupWithNavController(navController) + } + } + private fun showNavigation(visible: Boolean) { binding.navigationBar.animate().apply { if (visible) { @@ -138,4 +179,150 @@ class MainActivity : AppCompatActivity() { binding.statusBarShade.layoutParams = mlpShade windowInsets } + + 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() + + Toast.makeText( + applicationContext, + R.string.games_dir_selected, + Toast.LENGTH_LONG + ).show() + + gamesViewModel.reloadGames(true) + } + + 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(applicationContext, result, dstPath, "prod.keys")) { + if (NativeLibrary.reloadKeys()) { + Toast.makeText( + applicationContext, + R.string.install_keys_success, + Toast.LENGTH_SHORT + ).show() + gamesViewModel.reloadGames(true) + } else { + Toast.makeText( + applicationContext, + R.string.install_keys_failure, + Toast.LENGTH_LONG + ).show() + } + } + } + + 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( + applicationContext, + result, + dstPath, + "key_retail.bin" + ) + ) { + if (NativeLibrary.reloadKeys()) { + Toast.makeText( + applicationContext, + R.string.install_keys_success, + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + applicationContext, + R.string.install_amiibo_keys_failure, + Toast.LENGTH_LONG + ).show() + } + } + } + + 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/res/drawable/ic_arrow_forward.xml b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml new file mode 100644 index 000000000..3b85a3e2c --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_check.xml b/src/android/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 000000000..04b89abf2 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_controller.xml b/src/android/app/src/main/res/drawable/ic_controller.xml index 2359c35be..060cd9ae2 100644 --- a/src/android/app/src/main/res/drawable/ic_controller.xml +++ b/src/android/app/src/main/res/drawable/ic_controller.xml @@ -4,6 +4,6 @@ android:viewportHeight="24" android:viewportWidth="24"> diff --git a/src/android/app/src/main/res/drawable/ic_key.xml b/src/android/app/src/main/res/drawable/ic_key.xml new file mode 100644 index 000000000..a3943634f --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_key.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_title.xml b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml new file mode 100644 index 000000000..b733e5248 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml new file mode 100644 index 000000000..e05af9bdd --- /dev/null +++ b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout-w600dp/page_setup.xml b/src/android/app/src/main/res/layout-w600dp/page_setup.xml new file mode 100644 index 000000000..e1c26b2f8 --- /dev/null +++ b/src/android/app/src/main/res/layout-w600dp/page_setup.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + 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 68a3eae46..59812ab8e 100644 --- a/src/android/app/src/main/res/layout/activity_main.xml +++ b/src/android/app/src/main/res/layout/activity_main.xml @@ -24,10 +24,12 @@ android:id="@+id/navigation_bar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" - app:menu="@menu/menu_navigation" /> + app:menu="@menu/menu_navigation" + tools:visibility="visible" /> + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml new file mode 100644 index 000000000..965019cdb --- /dev/null +++ b/src/android/app/src/main/res/layout/page_setup.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index e85e24a85..5afa901c2 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -14,4 +14,13 @@ android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment" android:label="OptionsFragment" /> + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 564bad081..916f516c0 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -9,12 +9,28 @@ yuzu Switch emulator notifications yuzu is running + + Welcome! + Learn how to setup <b>yuzu</b> and jump into emulation. + Get started + Keys + Select your <b>prod.keys</b> file with the button below. + Select Keys + Games + Select your <b>Games</b> folder with the button below. + Done + You\'re all set.\nEnjoy your games! + Continue + Next + Back + Games Options Add Games Select your games folder Search Games + Games directory selected Install Prod.keys Required to decrypt retail games Install Amiibo Keys -- cgit v1.2.3