From 8bd0521b58a94594159030cbfc44198eb59aae18 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 12 Aug 2023 16:53:14 -0400 Subject: [PATCH] android: Show complete indicator during setup --- .../yuzu/yuzu_emu/adapters/SetupAdapter.kt | 19 ++- .../yuzu/yuzu_emu/fragments/SetupFragment.kt | 109 +++++++++------ .../java/org/yuzu/yuzu_emu/model/SetupPage.kt | 14 +- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 127 +++++++++--------- .../java/org/yuzu/yuzu_emu/utils/ViewUtils.kt | 35 +++++ .../src/main/res/layout-w600dp/page_setup.xml | 69 +++++++--- .../app/src/main/res/layout/page_setup.xml | 30 ++++- .../app/src/main/res/values/strings.xml | 1 + 8 files changed, 274 insertions(+), 130 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt 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 481ddd5a5..0e3cec9ac 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,17 @@ 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.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import org.yuzu.yuzu_emu.databinding.PageSetupBinding +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 +30,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 +39,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 +72,14 @@ 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) + } } } 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 6c4ddaf6b..8ca768dcf 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 @@ -32,10 +32,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 +115,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 +144,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 +171,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 +183,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 + } } ) ) @@ -194,15 +218,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 +239,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 +289,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 +308,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 +336,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 } 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 a0c878e1c..09a128ae6 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 f7d7aed1e..f77d06262 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 000000000..f9a3e4126 --- /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/page_setup.xml b/src/android/app/src/main/res/layout-w600dp/page_setup.xml index e1c26b2f8..9e0ab8ecb 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/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml index 1436ef308..535abcf02 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