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 index 481ddd5a5f..6b46d359e5 100644 --- 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 @@ -5,13 +5,19 @@ package org.yuzu.yuzu_emu.adapters import android.text.Html 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.lifecycle.ViewModelProvider 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.HomeViewModel +import org.yuzu.yuzu_emu.model.SetupCallback import org.yuzu.yuzu_emu.model.SetupPage +import org.yuzu.yuzu_emu.model.StepState +import org.yuzu.yuzu_emu.utils.ViewUtils class SetupAdapter(val activity: AppCompatActivity, val pages: List) : RecyclerView.Adapter() { @@ -26,7 +32,7 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List) holder.bind(pages[position]) inner class SetupPageViewHolder(val binding: PageSetupBinding) : - RecyclerView.ViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root), SetupCallback { lateinit var page: SetupPage init { @@ -35,6 +41,12 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List) fun bind(page: SetupPage) { this.page = page + + if (page.stepCompleted.invoke() == StepState.COMPLETE) { + binding.buttonAction.visibility = View.INVISIBLE + binding.textConfirmation.visibility = View.VISIBLE + } + binding.icon.setImageDrawable( ResourcesCompat.getDrawable( activity.resources, @@ -62,9 +74,15 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List) MaterialButton.ICON_GRAVITY_END } setOnClickListener { - page.buttonAction.invoke() + page.buttonAction.invoke(this@SetupPageViewHolder) } } } + + override fun onStepCompleted() { + ViewUtils.hideView(binding.buttonAction, 200) + ViewUtils.showView(binding.textConfirmation, 200) + ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index 6c4ddaf6b6..d50c421a01 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -19,6 +19,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController @@ -32,10 +33,13 @@ 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.SetupCallback import org.yuzu.yuzu_emu.model.SetupPage +import org.yuzu.yuzu_emu.model.StepState import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.GameHelper +import org.yuzu.yuzu_emu.utils.ViewUtils class SetupFragment : Fragment() { private var _binding: FragmentSetupBinding? = null @@ -112,14 +116,22 @@ class SetupFragment : Fragment() { 0, false, R.string.give_permission, - { permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) }, + { + notificationCallback = it + permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + }, true, R.string.notification_warning, R.string.notification_warning_description, 0, { - NotificationManagerCompat.from(requireContext()) + if (NotificationManagerCompat.from(requireContext()) .areNotificationsEnabled() + ) { + StepState.COMPLETE + } else { + StepState.INCOMPLETE + } } ) ) @@ -133,12 +145,22 @@ class SetupFragment : Fragment() { R.drawable.ic_add, true, R.string.select_keys, - { mainActivity.getProdKey.launch(arrayOf("*/*")) }, + { + keyCallback = it + getProdKey.launch(arrayOf("*/*")) + }, true, R.string.install_prod_keys_warning, R.string.install_prod_keys_warning_description, R.string.install_prod_keys_warning_help, - { File(DirectoryInitialization.userDirectory + "/keys/prod.keys").exists() } + { + val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys") + if (file.exists()) { + StepState.COMPLETE + } else { + StepState.INCOMPLETE + } + } ) ) add( @@ -150,9 +172,8 @@ class SetupFragment : Fragment() { true, R.string.add_games, { - mainActivity.getGamesDirectory.launch( - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data - ) + gamesDirCallback = it + getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, true, R.string.add_games_warning, @@ -163,7 +184,11 @@ class SetupFragment : Fragment() { PreferenceManager.getDefaultSharedPreferences( YuzuApplication.appContext ) - preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty() + if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) { + StepState.COMPLETE + } else { + StepState.INCOMPLETE + } } ) ) @@ -181,6 +206,13 @@ class SetupFragment : Fragment() { ) } + homeViewModel.shouldPageForward.observe(viewLifecycleOwner) { + if (it) { + pageForward() + homeViewModel.setShouldPageForward(false) + } + } + binding.viewPager2.apply { adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) offscreenPageLimit = 2 @@ -194,15 +226,15 @@ class SetupFragment : Fragment() { super.onPageSelected(position) if (position == 1 && previousPosition == 0) { - showView(binding.buttonNext) - showView(binding.buttonBack) + ViewUtils.showView(binding.buttonNext) + ViewUtils.showView(binding.buttonBack) } else if (position == 0 && previousPosition == 1) { - hideView(binding.buttonBack) - hideView(binding.buttonNext) + ViewUtils.hideView(binding.buttonBack) + ViewUtils.hideView(binding.buttonNext) } else if (position == pages.size - 1 && previousPosition == pages.size - 2) { - hideView(binding.buttonNext) + ViewUtils.hideView(binding.buttonNext) } else if (position == pages.size - 2 && previousPosition == pages.size - 1) { - showView(binding.buttonNext) + ViewUtils.showView(binding.buttonNext) } previousPosition = position @@ -215,7 +247,8 @@ class SetupFragment : Fragment() { // Checks if the user has completed the task on the current page if (currentPage.hasWarning) { - if (currentPage.taskCompleted.invoke()) { + val stepState = currentPage.stepCompleted.invoke() + if (stepState != StepState.INCOMPLETE) { pageForward() return@setOnClickListener } @@ -264,9 +297,15 @@ class SetupFragment : Fragment() { _binding = null } + private lateinit var notificationCallback: SetupCallback + @RequiresApi(Build.VERSION_CODES.TIRAMISU) private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) { + notificationCallback.onStepCompleted() + } + if (!it && !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) ) { @@ -277,6 +316,27 @@ class SetupFragment : Fragment() { } } + private lateinit var keyCallback: SetupCallback + + val getProdKey = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + if (mainActivity.processKey(result)) { + keyCallback.onStepCompleted() + } + } + } + + private lateinit var gamesDirCallback: SetupCallback + + val getGamesDirectory = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> + if (result != null) { + mainActivity.processGamesDir(result) + gamesDirCallback.onStepCompleted() + } + } + private fun finishSetup() { PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) @@ -284,33 +344,6 @@ class SetupFragment : Fragment() { mainActivity.finishSetup(binding.root.findNavController()) } - private fun showView(view: View) { - 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.INVISIBLE) { - return - } - - view.apply { - alpha = 1f - isClickable = false - }.animate().apply { - duration = 300 - alpha(0f) - }.withEndAction { - view.visibility = View.INVISIBLE - } - } - fun pageForward() { binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1 } @@ -326,15 +359,29 @@ class SetupFragment : Fragment() { private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener( binding.root - ) { view: View, windowInsets: WindowInsetsCompat -> + ) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) - view.setPadding( - barInsets.left + cutoutInsets.left, - barInsets.top + cutoutInsets.top, - barInsets.right + cutoutInsets.right, - barInsets.bottom + cutoutInsets.bottom - ) + + val leftPadding = barInsets.left + cutoutInsets.left + val topPadding = barInsets.top + cutoutInsets.top + val rightPadding = barInsets.right + cutoutInsets.right + val bottomPadding = barInsets.bottom + cutoutInsets.bottom + + if (resources.getBoolean(R.bool.small_layout)) { + binding.viewPager2 + .updatePadding(left = leftPadding, top = topPadding, right = rightPadding) + binding.constraintButtons + .updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding) + } else { + binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding) + binding.constraintButtons + .updatePadding( + left = leftPadding, + right = rightPadding, + bottom = bottomPadding + ) + } 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 263ee7144d..e13d84c9ca 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 @@ -14,6 +14,9 @@ class HomeViewModel : ViewModel() { private val _statusBarShadeVisible = MutableLiveData(true) val statusBarShadeVisible: LiveData get() = _statusBarShadeVisible + private val _shouldPageForward = MutableLiveData(false) + val shouldPageForward: LiveData get() = _shouldPageForward + var navigatedToSetup = false init { @@ -33,4 +36,8 @@ class HomeViewModel : ViewModel() { } _statusBarShadeVisible.value = visible } + + fun setShouldPageForward(pageForward: Boolean) { + _shouldPageForward.value = pageForward + } } 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 index a0c878e1ca..09a128ae65 100644 --- 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 @@ -10,10 +10,20 @@ data class SetupPage( val buttonIconId: Int, val leftAlignedIcon: Boolean, val buttonTextId: Int, - val buttonAction: () -> Unit, + val buttonAction: (callback: SetupCallback) -> Unit, val hasWarning: Boolean, val warningTitleId: Int = 0, val warningDescriptionId: Int = 0, val warningHelpLinkId: Int = 0, - val taskCompleted: () -> Boolean = { true } + val stepCompleted: () -> StepState = { StepState.UNDEFINED } ) + +interface SetupCallback { + fun onStepCompleted() +} + +enum class StepState { + COMPLETE, + INCOMPLETE, + UNDEFINED +} 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 f7d7aed1e4..f77d062620 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 @@ -266,74 +266,81 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getGamesDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> - if (result == null) { - return@registerForActivityResult + if (result != null) { + processGamesDir(result) } - - contentResolver.takePersistableUriPermission( - result, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - - // 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) } + fun processGamesDir(result: Uri) { + contentResolver.takePersistableUriPermission( + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + // 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 - } - - if (FileUtil.getExtension(result) != "keys") { - MessageDialogFragment.newInstance( - R.string.reading_keys_failure, - R.string.install_prod_keys_failure_extension_description - ).show(supportFragmentManager, MessageDialogFragment.TAG) - return@registerForActivityResult - } - - contentResolver.takePersistableUriPermission( - result, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - - 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 { - MessageDialogFragment.newInstance( - R.string.invalid_keys_error, - R.string.install_keys_failure_description, - R.string.dumping_keys_quickstart_link - ).show(supportFragmentManager, MessageDialogFragment.TAG) - } + if (result != null) { + processKey(result) } } + fun processKey(result: Uri): Boolean { + if (FileUtil.getExtension(result) != "keys") { + MessageDialogFragment.newInstance( + R.string.reading_keys_failure, + R.string.install_prod_keys_failure_extension_description + ).show(supportFragmentManager, MessageDialogFragment.TAG) + return false + } + + contentResolver.takePersistableUriPermission( + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + 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) + return true + } else { + MessageDialogFragment.newInstance( + R.string.invalid_keys_error, + R.string.install_keys_failure_description, + R.string.dumping_keys_quickstart_link + ).show(supportFragmentManager, MessageDialogFragment.TAG) + return false + } + } + return false + } + val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result == null) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt new file mode 100644 index 0000000000..f9a3e4126b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.view.View + +object ViewUtils { + fun showView(view: View, length: Long = 300) { + view.apply { + alpha = 0f + visibility = View.VISIBLE + isClickable = true + }.animate().apply { + duration = length + alpha(1f) + }.start() + } + + fun hideView(view: View, length: Long = 300) { + if (view.visibility == View.INVISIBLE) { + return + } + + view.apply { + alpha = 1f + isClickable = false + }.animate().apply { + duration = length + alpha(0f) + }.withEndAction { + view.visibility = View.INVISIBLE + }.start() + } +} 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 index cbe631d888..406df9eabd 100644 --- a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml +++ b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml @@ -1,5 +1,5 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentTop="true" + android:layout_alignParentBottom="true" + android:clipToPadding="false" /> - + android:layout_alignParentBottom="true" + android:layout_margin="8dp"> - + - + + + + + 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 index e1c26b2f8d..9e0ab8ecb1 100644 --- a/src/android/app/src/main/res/layout-w600dp/page_setup.xml +++ b/src/android/app/src/main/res/layout-w600dp/page_setup.xml @@ -21,45 +21,76 @@ - + android:layout_weight="1"> + + - + diff --git a/src/android/app/src/main/res/layout/fragment_setup.xml b/src/android/app/src/main/res/layout/fragment_setup.xml index d7bafaea24..9499f64636 100644 --- a/src/android/app/src/main/res/layout/fragment_setup.xml +++ b/src/android/app/src/main/res/layout/fragment_setup.xml @@ -1,5 +1,5 @@ - - - + android:layout_above="@+id/constraint_buttons" + android:layout_alignParentTop="true" + android:clipToPadding="false" /> - + android:layout_margin="8dp" + android:layout_alignParentBottom="true"> - + + + + + + + diff --git a/src/android/app/src/main/res/layout/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml index 1436ef308a..535abcf029 100644 --- a/src/android/app/src/main/res/layout/page_setup.xml +++ b/src/android/app/src/main/res/layout/page_setup.xml @@ -21,11 +21,12 @@ app:layout_constraintVertical_chainStyle="spread" app:layout_constraintWidth_max="220dp" app:layout_constraintWidth_min="110dp" - app:layout_constraintVertical_weight="3" /> + app:layout_constraintVertical_weight="3" + tools:src="@drawable/ic_notification" /> + + Back Add Games Select your games folder + Complete! Games